[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug report\ndescription: Create reports to help us improve\nlabels: bug\nbody:\n  - type: dropdown\n    attributes:\n      label: Operating system\n      description: Operating system type\n      options:\n        - iOS\n        - macOS\n        - Android\n        - Windows\n        - Linux\n        - All\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: 描述错误(Describe the bug)\n      description: Please provide a detailed description of the error.\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: To Reproduce\n      description: \"重现行为的步骤: 如具体应用抓包失败，请说明软件名称以及具体操作页面.\"\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature request\ndescription: Suggest an idea for this project\nlabels: enhancement\nbody:\n  - type: dropdown\n    attributes:\n      label: Operating system\n      description: Operating system type\n      options:\n        - iOS\n        - macOS\n        - Android\n        - Windows\n        - Linux\n        - All\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Is your feature request related to a problem? Please describe.\n      description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Describe the solution you'd like\n      description: A clear and concise description of what you want to happen.\n\n  - type: checkboxes\n    id: supporter\n    attributes:\n      label: Supporter\n      options:\n        - label: I am a [sponsor](https://buymeacoffee.com/proxypin)\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/功能请求.yml",
    "content": "name: 功能请求\ndescription: 为这个项目提出一个想法\nlabels: enhancement\nbody:\n  - type: dropdown\n    attributes:\n      label: Operating system\n      description: Operating system type\n      options:\n        - iOS\n        - macOS\n        - Android\n        - Windows\n        - Linux\n        - All\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: 您的功能请求是否与某个问题相关？请描述.\n      description: 对问题所在的清晰简洁的描述.\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: 描述您想要的解决方案\n      description: 对您想要的解决方案的清晰简洁的描述.\n\n  - type: checkboxes\n    id: supporter\n    attributes:\n      label: 支持我们\n      options:\n        - label: 我已经 [赞助](https://afdian.com/a/proxypin)"
  },
  {
    "path": ".gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.build/\n.buildlog/\n.history\n.svn/\n.swiftpm/\nmigrate_working_dir/\nPodfile.lock\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n\nl10n_errors.txt\npubspec.lock\n/dist/"
  },
  {
    "path": ".metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled.\n\nversion:\n  revision: 796c8ef79279f9c774545b3771238c3098dbefab\n  channel: stable\n\nproject_type: app\n\n# Tracks metadata for the flutter migrate command\nmigration:\n  platforms:\n    - platform: root\n      create_revision: 796c8ef79279f9c774545b3771238c3098dbefab\n      base_revision: 796c8ef79279f9c774545b3771238c3098dbefab\n    - platform: ios\n      create_revision: 796c8ef79279f9c774545b3771238c3098dbefab\n      base_revision: 796c8ef79279f9c774545b3771238c3098dbefab\n\n  # User provided section\n\n  # List of Local paths (relative to this file) that should be\n  # ignored by the migrate tool.\n  #\n  # Files that are not part of the templates will be ignored by default.\n  unmanaged_files:\n    - 'lib/main.dart'\n    - 'ios/Runner.xcodeproj/project.pbxproj'\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": "# ProxyPin\n\nEnglish | [中文](README_CN.md)\n## Open source free traffic capture HTTP(S)，Support Windows、Mac、Android、IOS、Linux Full platform system\n\nYou can use it to intercept, inspect & rewrite HTTP(S) traffic, Support capturing Flutter app traffic, ProxyPin is based on Flutter develop, and the UI is beautiful\nand easy to use.\n\n## Features\n* Mobile scan code connection: no need to manually configure WiFi proxy, including configuration synchronization. All terminals can scan codes to connect and forward traffic to each other.\n* Domain name filtering: Only intercept the traffic you need, and do not intercept other traffic to avoid interference with other applications.\n* Search: Search requests according to keywords, response types and other conditions\n* Script: Support writing JavaScript scripts to process requests or responses.\n* Request Rewrite: Support redirection, support replacement of request or response message, and can also modify request or response according to the increase.\n* Request Mapping: Do not request remote services, use local configuration or scripts for response\n* Request Decryption: Configure AES decryption key to automatically decrypt HTTP message body\n* Request Blocking: Support blocking requests according to URL, and do not send requests to the server.\n* History: Automatically save the captured traffic data for easy backtracking and viewing. Support HAR format export and import.\n* Others: Favorites, toolbox, common encoding tools, as well as QR codes, regular expressions, etc.\n\n**Mac will prompt untrusted developers when first opened, you need to go to System Preferences-Security & Privacy-Allow any source.**\n\n## Sponsors\n\nIf ProxyPin is helpful to you, you are welcome to support us in the following ways to help the project develop in the long term:\n\n* [Buy Me A Coffee](https://buymeacoffee.com/proxypin)\n* [AFDIAN](https://afdian.com/a/proxypin)\n* Submit feedback and suggestions to help us improve\n* Contribute code or documentation to the project\n\n**Your support will be used for project maintenance, feature development, and user experience optimization. Thank you very much!**\n\n## Downloads\n\nGithub Releases: https://github.com/wanghongenpin/proxypin/releases\n\niOS App Store：https://apps.apple.com/app/proxypin/id6450932949\n\nAndroid Google Play：https://play.google.com/store/apps/details?id=com.network.proxy\n\nTG: https://t.me/proxypin_en\n\n**We will continue to improve the features and experience, as well as optimize the UI.**\n\n<img alt=\"image\"  width=\"580px\" height=\"420px\"  src=\"https://github.com/user-attachments/assets/6c1345ab-c95c-415d-ac59-470c764b59a2\">.<img alt=\"image\"  height=\"500px\" src=\"https://github.com/user-attachments/assets/3c5572b0-a9e5-497c-8b42-f935e836c164\">\n"
  },
  {
    "path": "README_CN.md",
    "content": "# ProxyPin\n\n[English](README.md) | 中文\n## 开源免费抓包工具，支持Windows、Mac、Android、IOS、Linux 全平台系统\n\n您可以使用它来拦截、检查和重写HTTP（S）流量，支持Flutter应用抓包，ProxyPin基于Flutter开发，UI美观易用。\n\n## 核心特性\n\n* 手机扫码连接: 不用手动配置Wifi代理，包括配置同步。所有终端都可以互相扫码连接转发流量。\n* 域名过滤: 只拦截您所需要的流量，不拦截其他流量，避免干扰其他应用。\n* 搜索：根据关键词响应类型多种条件搜索请求\n* 脚本: 支持编写JavaScript脚本来处理请求或响应。\n* 请求重写: 支持重定向，支持替换请求或响应报文，也可以根据增则修改请求或或响应。\n* 请求映射: 不请求远程服务，使用本地配置或脚本进行响应\n* 请求解密: 配置AES解密密钥，自动解密HTTP消息体\n* 请求屏蔽: 支持根据URL屏蔽请求，不让请求发送到服务器。\n* 历史记录：自动保存抓包的流量数据，方便回溯查看。支持HAR格式导出与导入。\n* 其他：收藏、工具箱、常用编码工具、以及二维码、正则等\n\n**Mac首次打开会提示不受信任开发者，需要到系统偏好设置-安全性与隐私-允许任何来源。**\n\n## 赞助 \n\n如果您觉得ProxyPin对您有帮助，欢迎通过以下方式支持我们，帮助项目长期发展：\n\n* [爱发电赞助](https://afdian.com/a/proxypin)\n* [Buy Me A Coffee](https://buymeacoffee.com/proxypin)\n* 提交反馈和建议，帮助我们改进\n* 为项目贡献代码或文档\n\n**您的支持将用于项目的维护、功能开发和用户体验优化，非常感谢！**    \n\n## 下载地址\n\n国内下载： https://gitee.com/wanghongenpin/proxypin/releases\n\niOS App Store： https://apps.apple.com/app/proxypin/id6450932949      \n\nAndroid Google Play：https://play.google.com/store/apps/details?id=com.network.proxy\n\nTG: https://t.me/proxypin_tg\n\n**接下来会持续完善功能和体验，UI优化。**\n\n<img alt=\"image\"  width=\"580px\" height=\"420px\"  src=\"https://github.com/user-attachments/assets/80f30d64-f2b5-473c-98f5-bae50b309278\">.<img alt=\"image\"  height=\"500px\" src=\"https://github.com/user-attachments/assets/3c5572b0-a9e5-497c-8b42-f935e836c164\">\n\n"
  },
  {
    "path": "analysis_options.yaml",
    "content": "# The following line activates a set of recommended lints for Flutter apps,\n# packages, and plugins designed to encourage good coding practices.\ninclude: package:flutter_lints/flutter.yaml\n\nformatter:\n  page_width: 120\n\nlinter:\n  # The lint rules applied to this project can be customized in the\n  # section below to disable rules from the `package:flutter_lints/flutter.yaml`\n  # included above or to enable additional rules. A list of all available lints\n  # and their documentation is published at\n  # https://dart-lang.github.io/linter/lints/index.html.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n     avoid_print: false\n    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n# Remember to never publicly share your keystore.\n# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app\nkey.properties\n**/*.keystore\n**/*.jks\n/app/.cxx/"
  },
  {
    "path": "android/app/build.gradle",
    "content": "import java.util.Properties\nimport java.io.FileInputStream\nimport java.io.File\n\nplugins {\n    id \"com.android.application\"\n    id \"kotlin-android\"\n    id \"dev.flutter.flutter-gradle-plugin\"\n}\n\ndef localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertiesFile.exists()) {\n    localPropertiesFile.withReader('UTF-8') { reader ->\n        localProperties.load(reader)\n    }\n}\n\ndef flutterVersionCode = localProperties.getProperty('flutter.versionCode')\nif (flutterVersionCode == null) {\n    flutterVersionCode = '1'\n}\n\ndef flutterVersionName = localProperties.getProperty('flutter.versionName')\nif (flutterVersionName == null) {\n    flutterVersionName = '1.0'\n}\n\ndef keystoreProperties = new Properties()\ndef keystorePropertiesFile = rootProject.file('key.properties')\nif (keystorePropertiesFile.exists()) {\n    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))\n}\n\nandroid {\n    namespace \"com.network.proxy\"\n    compileSdk flutter.compileSdkVersion\n    ndkVersion flutter.ndkVersion\n//    ndkVersion \"26.1.10909125\"\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n    }\n\n    packagingOptions {\n        dex {\n            useLegacyPackaging true\n        }\n        jniLibs {\n            useLegacyPackaging true\n        }\n    }\n\n    defaultConfig {\n        applicationId \"com.network.proxy\"\n        ndk { abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64' }\n        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.\n        minSdkVersion flutter.minSdkVersion\n        targetSdkVersion flutter.targetSdkVersion\n        multiDexEnabled true\n        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n    }\n\n    signingConfigs {\n        release {\n            keyAlias keystoreProperties['keyAlias']\n            keyPassword keystoreProperties['keyPassword']\n            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null\n            storePassword keystoreProperties['storePassword']\n        }\n    }\n\n    buildTypes {\n        release {\n            // Signing with the debug keys for now, so `flutter run --release` works.\n            signingConfig signingConfigs.release\n\n            minifyEnabled true\n            shrinkResources true\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n        debug {\n            signingConfig signingConfigs.release\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\nflutter {\n    source '../..'\n}\n\n// Set a fixed APK name for release builds\n// Use the older ApplicationVariants API which is compatible across AGP versions\nandroid.applicationVariants.all { variant ->\n    if (variant.buildType.name == 'release') {\n        variant.outputs.all { output ->\n            def apkName = \"proxypin-android.apk\"\n            try {\n                // Newer output API\n                output.outputFileName = apkName\n            } catch (Exception e) {\n                // Fallback for older API\n                output.outputFile = new File(output.outputFile.parent, apkName)\n            }\n        }\n    }\n}\n\ndependencies {\n}\n"
  },
  {
    "path": "android/app/proguard-rules.pro",
    "content": "#Flutter Wrapper\n-keep class io.flutter.app.** { *; }\n-keep class io.flutter.plugin.**  { *; }\n-keep class io.flutter.util.**  { *; }\n-keep class io.flutter.view.**  { *; }\n-keep class io.flutter.**  { *; }\n-keep class io.flutter.plugins.**  { *; }\n-keep class de.prosiebensat1digital.** { *; }\n\n-dontwarn com.google.android.play.core.**\n"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\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\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.VIBRATE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n\n    <uses-permission android:name=\"android.permission.QUERY_ALL_PACKAGES\"\n        tools:ignore=\"QueryAllPackagesPermission\" />\n\n    <!-- Declare media/storage permissions. These are required if your app or\n         plugins directly read media files. You still need to request these at\n         runtime on Android (and on Android 13+ use the READ_MEDIA_* permissions).\n         If you prefer to avoid declaring sensitive permissions, use the system\n         picker / SAF which doesn't require these permissions. -->\n    <!-- These permissions are removed to comply with Google Play's \"Photos and videos\" policy.\n         Use the system photo picker / SAF to avoid requesting broad media access. -->\n    <uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\" tools:node=\"remove\" />\n    <uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\" tools:node=\"remove\" />\n    <uses-permission android:name=\"android.permission.READ_MEDIA_AUDIO\" tools:node=\"remove\" />\n\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" tools:node=\"remove\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" tools:node=\"remove\" />\n\n    <application\n        android:name=\"${applicationName}\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"ProxyPin\">\n        <activity\n            android:name=\"com.network.proxy.MainActivity\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n            android:exported=\"true\"\n            android:hardwareAccelerated=\"true\"\n            android:launchMode=\"singleTop\"\n            android:supportsPictureInPicture=\"true\"\n            android:theme=\"@style/LaunchTheme\"\n            android:windowSoftInputMode=\"adjustResize\"\n            tools:targetApi=\"n\">\n            <!-- Specifies an Android theme to apply to this Activity as soon as\n                 the Android process has started. This theme is visible to the user\n                 while the Flutter UI initializes. After that, this theme continues\n                 to determine the Window background behind the Flutter UI. -->\n            <meta-data\n                android:name=\"io.flutter.embedding.android.NormalTheme\"\n                android:resource=\"@style/NormalTheme\" />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n\n        <service\n            android:name=\".ProxyVpnService\"\n            android:exported=\"true\"\n            android:permission=\"android.permission.BIND_VPN_SERVICE\">\n            <intent-filter>\n                <action android:name=\"android.net.VpnService\" />\n            </intent-filter>\n        </service>\n\n        <activity\n            android:name=\".VpnAlertDialog\"\n            android:exported=\"false\">\n            <intent-filter>\n                <action android:name=\"com.network.proxy.ProxyVpnService\" />\n            </intent-filter>\n        </activity>\n        <!-- Don't delete the meta-data below.\n             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->\n        <meta-data\n            android:name=\"flutterEmbedding\"\n            android:value=\"2\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/MainActivity.kt",
    "content": "package com.network.proxy\n\nimport android.content.Intent\nimport android.content.res.Configuration\nimport com.network.proxy.plugin.AppLifecyclePlugin\nimport com.network.proxy.plugin.InstalledAppsPlugin\nimport com.network.proxy.plugin.PictureInPicturePlugin\nimport com.network.proxy.plugin.ProcessInfoPlugin\nimport com.network.proxy.plugin.VpnServicePlugin\nimport io.flutter.embedding.android.FlutterActivity\nimport io.flutter.embedding.engine.FlutterEngine\n\n\nclass MainActivity : FlutterActivity() {\n    private val lifecycleChannel: AppLifecyclePlugin = AppLifecyclePlugin()\n\n    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\n        super.configureFlutterEngine(flutterEngine)\n        pluginRegister(flutterEngine)\n    }\n\n    override fun onUserLeaveHint() {\n        super.onUserLeaveHint()\n        lifecycleChannel.onUserLeaveHint()\n    }\n\n    override fun onPictureInPictureModeChanged(\n        isInPictureInPictureMode: Boolean,\n        newConfig: Configuration?\n    ) {\n        lifecycleChannel.onPictureInPictureModeChanged(isInPictureInPictureMode)\n        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)\n    }\n\n    /**\n     * 注册插件\n     */\n    private fun pluginRegister(flutterEngine: FlutterEngine) {\n        flutterEngine.plugins.add(VpnServicePlugin())\n        flutterEngine.plugins.add(PictureInPicturePlugin())\n        flutterEngine.plugins.add(lifecycleChannel)\n        flutterEngine.plugins.add(InstalledAppsPlugin())\n        flutterEngine.plugins.add(ProcessInfoPlugin())\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        if (requestCode == VpnServicePlugin.REQUEST_CODE) {\n            if (resultCode == RESULT_OK) {\n                activity.startService(ProxyVpnService.startVpnIntent(activity))\n                return\n            }\n\n            val alertDialog = Intent(applicationContext, VpnAlertDialog::class.java)\n                .setAction(\"com.network.proxy.ProxyVpnService\")\n            alertDialog.flags = Intent.FLAG_ACTIVITY_NEW_TASK\n            startActivity(alertDialog)\n            return\n        }\n\n        super.onActivityResult(requestCode, resultCode, data)\n    }\n\n    override fun onDestroy() {\n//        activity.startService(ProxyVpnService.stopVpnIntent(activity))\n        super.onDestroy()\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt",
    "content": "package com.network.proxy\n\nimport android.app.Activity\nimport android.app.Notification\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.net.IpPrefix\nimport android.net.ProxyInfo\nimport android.net.VpnService\nimport android.os.Build\nimport android.os.ParcelFileDescriptor\nimport android.util.Log\nimport androidx.annotation.RequiresApi\nimport androidx.core.app.NotificationCompat\nimport com.network.proxy.plugin.VpnServicePlugin.Companion.REQUEST_CODE\nimport com.network.proxy.vpn.ProxyVpnThread\nimport com.network.proxy.vpn.socket.ProtectSocket\nimport com.network.proxy.vpn.socket.ProtectSocketHolder\nimport java.net.InetAddress\n\n/**\n * VPN服务\n * @author wanghongen\n */\nclass ProxyVpnService : VpnService(), ProtectSocket {\n    private var vpnInterface: ParcelFileDescriptor? = null\n    private var vpnThread: ProxyVpnThread? = null\n\n    companion object {\n        const val MAX_PACKET_LEN = 1500\n\n        const val VIRTUAL_HOST = \"10.0.0.2\"\n\n        const val PROXY_HOST_KEY = \"ProxyHost\"\n        const val PROXY_PORT_KEY = \"ProxyPort\"\n        const val ALLOW_APPS_KEY = \"AllowApps\" //允许的名单\n        const val DISALLOW_APPS_KEY = \"DisallowApps\" //禁止的名单\n        const val SET_SYSTEM_PROXY_KEY = \"SetSystemProxy\"\n        const val PROXY_PASS_DOMAINS_KEY = \"ProxyPassDomains\"\n\n        /**\n         * 动作：断开连接\n         */\n        const val ACTION_DISCONNECT = \"DISCONNECT\"\n\n        /**\n         * 通知配置\n         */\n        private const val NOTIFICATION_ID = 9527\n        const val VPN_NOTIFICATION_CHANNEL_ID = \"vpn-notifications\"\n\n        var isRunning = false\n\n        var host: String? = null\n        var port: Int = 9099\n        var allowApps: ArrayList<String>? = null\n        var disallowApps: ArrayList<String>? = null\n        var setSystemProxy: Boolean = true\n\n        var proxyPassDomains: ArrayList<String>? = null\n\n        fun stopVpnIntent(context: Context): Intent {\n            return Intent(context, ProxyVpnService::class.java).also {\n                it.action = ACTION_DISCONNECT\n            }\n        }\n\n        fun startVpnIntent(\n            context: Context,\n            proxyHost: String? = host,\n            proxyPort: Int? = port,\n            allowApps: ArrayList<String>? = this.allowApps,\n            disallowApps: ArrayList<String>? = this.disallowApps,\n            setSystemProxy: Boolean = true,\n            proxyPassDomains: ArrayList<String>? = null\n        ): Intent {\n            return Intent(context, ProxyVpnService::class.java).also {\n                it.putExtra(PROXY_HOST_KEY, proxyHost)\n                it.putExtra(PROXY_PORT_KEY, proxyPort)\n                it.putStringArrayListExtra(ALLOW_APPS_KEY, allowApps)\n                it.putStringArrayListExtra(DISALLOW_APPS_KEY, disallowApps)\n                it.putExtra(SET_SYSTEM_PROXY_KEY, setSystemProxy)\n                it.putStringArrayListExtra(PROXY_PASS_DOMAINS_KEY, proxyPassDomains)\n            }\n        }\n\n        /**\n         * 准备vpn<br>\n         * 设备可能弹出连接vpn提示\n         */\n        fun prepareVpn(\n            activity: Activity,\n            host: String,\n            port: Int,\n            allowApps: ArrayList<String>?,\n            disallowApps: ArrayList<String>?,\n            setSystemProxy: Boolean = true,\n            proxyPassDomains: ArrayList<String>? = null\n        ): Boolean {\n            val intent = prepare(activity)\n            if (intent != null) {\n                ProxyVpnService.host = host\n                ProxyVpnService.port = port\n                ProxyVpnService.allowApps = allowApps\n                ProxyVpnService.disallowApps = disallowApps\n                ProxyVpnService.setSystemProxy = setSystemProxy\n                ProxyVpnService.proxyPassDomains = proxyPassDomains\n\n                activity.startActivityForResult(intent, REQUEST_CODE)\n                return false\n            }\n            return true\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        disconnect()\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        if (intent == null) {\n            return START_NOT_STICKY\n        }\n\n        return if (intent.action == ACTION_DISCONNECT) {\n            disconnect()\n            START_NOT_STICKY\n        } else {\n            val proxyHost = intent.getStringExtra(PROXY_HOST_KEY) ?: (host ?: \"127.0.0.1\")\n            val proxyPort = intent.getIntExtra(PROXY_PORT_KEY, port)\n            val allowPackages =\n                intent.getStringArrayListExtra(ALLOW_APPS_KEY) ?: allowApps ?: ArrayList()\n            val disallowPackages =\n                intent.getStringArrayListExtra(DISALLOW_APPS_KEY) ?: disallowApps ?: ArrayList()\n            val setSystemProxy = intent.getBooleanExtra(SET_SYSTEM_PROXY_KEY, setSystemProxy)\n            val proxyPassDomains = intent.getStringArrayListExtra(PROXY_PASS_DOMAINS_KEY)\n\n            connect(\n                proxyHost,\n                proxyPort,\n                allowPackages,\n                disallowPackages,\n                setSystemProxy,\n                proxyPassDomains\n            )\n            START_STICKY\n        }\n    }\n\n    private fun disconnect() {\n        vpnThread?.run { stopThread() }\n        vpnInterface?.close()\n        stopForeground(STOP_FOREGROUND_REMOVE)\n        vpnInterface = null\n        isRunning = false\n    }\n\n    private fun connect(\n        proxyHost: String,\n        proxyPort: Int,\n        allowPackages: ArrayList<String>?,\n        disallowPackages: ArrayList<String>?,\n        setSystemProxy: Boolean = true,\n        proxyPassDomains: ArrayList<String>? = null\n    ) {\n        Log.i(\n            \"ProxyVpnService\",\n            \"startVpn $proxyHost:$proxyPort systemProxy: $setSystemProxy allowPackages: $allowPackages proxyPassDomains: $proxyPassDomains\"\n        )\n\n        host = proxyHost\n        port = proxyPort\n        allowApps = allowPackages\n        disallowApps = disallowPackages\n        ProxyVpnService.proxyPassDomains = proxyPassDomains\n        vpnInterface = createVpnInterface(\n            proxyHost,\n            proxyPort,\n            allowPackages,\n            disallowPackages,\n            setSystemProxy,\n            proxyPassDomains\n        )\n        if (vpnInterface == null) {\n            val alertDialog = Intent(applicationContext, VpnAlertDialog::class.java)\n                .setAction(\"com.network.proxy.ProxyVpnService\")\n            alertDialog.flags = Intent.FLAG_ACTIVITY_NEW_TASK\n            startActivity(alertDialog)\n            return\n        }\n\n        ProtectSocketHolder.setProtectSocket(this)\n        showServiceNotification()\n        vpnThread = ProxyVpnThread(\n            vpnInterface!!,\n            proxyHost,\n            proxyPort,\n            proxyPassDomains\n        )\n        vpnThread!!.start()\n        isRunning = true\n    }\n\n    private fun showServiceNotification() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager\n            val notificationChannel = NotificationChannel(\n                VPN_NOTIFICATION_CHANNEL_ID,\n                \"VPN Status\",\n                NotificationManager.IMPORTANCE_LOW\n            )\n            notificationManager.createNotificationChannel(notificationChannel)\n        }\n\n        val pendingActivityIntent: PendingIntent =\n            Intent(this, MainActivity::class.java).let { notificationIntent ->\n                PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)\n            }\n\n        val notification: Notification =\n            NotificationCompat.Builder(this, VPN_NOTIFICATION_CHANNEL_ID)\n                .setSmallIcon(R.mipmap.ic_launcher)\n                .setContentIntent(pendingActivityIntent)\n                .setContentTitle(getString(R.string.vpn_active_notification_title))\n                .setContentText(getString(R.string.vpn_active_notification_content))\n                .setOngoing(true)\n                .build()\n\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {\n            startForeground(NOTIFICATION_ID, notification)\n        }\n    }\n\n\n    private fun createVpnInterface(\n        proxyHost: String,\n        proxyPort: Int,\n        allowPackages: List<String>?,\n        disallowApps: ArrayList<String>?,\n        setSystemProxy: Boolean = true,\n        proxyPassDomains: ArrayList<String>? = null\n    ):\n            ParcelFileDescriptor? {\n        val build = Builder()\n            .setMtu(MAX_PACKET_LEN)\n            .addAddress(VIRTUAL_HOST, 32)\n            .addRoute(\"0.0.0.0\", 0)\n            .setSession(baseContext.applicationInfo.name)\n            .setBlocking(true)\n\n        // 处理 proxyPassDomains 中的 CIDR 格式，添加到 excludeRoute\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && proxyPassDomains != null) {\n            applyExcludeRoutes(build, proxyPassDomains)\n        }\n\n        val packages = allowPackages?.filter { it != baseContext.packageName }\n        if (packages?.isNotEmpty() == true) {\n            packages.forEach {\n                build.addAllowedApplication(it)\n            }\n        } else {\n            build.addDisallowedApplication(baseContext.packageName)\n        }\n\n        disallowApps?.forEach {\n            if (packages?.contains(it) == true) return@forEach\n            build.addDisallowedApplication(it)\n        }\n\n        build.setConfigureIntent(\n            PendingIntent.getActivity(\n                this,\n                0,\n                Intent(this, MainActivity::class.java),\n                PendingIntent.FLAG_IMMUTABLE\n            )\n        )\n\n        return build.apply {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                setMetered(false)\n            }\n\n            if (setSystemProxy && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                Log.d(\"ProxyVpnService\", \"set system proxy $proxyHost:$proxyPort\")\n                val buildProxy = ProxyInfo.buildDirectProxy(proxyHost, proxyPort)\n                setHttpProxy(buildProxy)\n            }\n        }.establish()\n    }\n\n    /**\n     * 应用排除路由规则\n     * 根据 proxyPassDomains 列表配置 VPN 的 excludeRoute\n     *\n     * @param builder VPN Builder 实例\n     * @param proxyPassDomains 需要排除的域名/IP列表\n     */\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    private fun applyExcludeRoutes(builder: Builder, proxyPassDomains: ArrayList<String>) {\n\n        proxyPassDomains.forEach { domain ->\n            try {\n                val trimmedDomain = domain.trim()\n                when {\n                    // 2. localhost 或 127.0.0.1\n                    trimmedDomain == \"localhost\" || trimmedDomain == \"127.0.0.1\" -> {\n                        Log.d(\"ProxyVpnService\", \"Skipped excludeRoute for localhost: $trimmedDomain\")\n                    }\n\n                    // 1. CIDR 格式：192.168.0.0/16\n                    trimmedDomain.contains(\"/\") -> {\n                        addCidrExcludeRoute(builder, trimmedDomain)\n                    }\n\n                    // 3. 单个 IP 地址（不含通配符）\n                    !trimmedDomain.contains(\"*\") && isValidIpAddress(trimmedDomain) -> {\n                        addSingleIpExcludeRoute(builder, trimmedDomain)\n                    }\n                    // 4. 域名和通配符域名会被跳过（不能用于 excludeRoute）\n                }\n            } catch (e: Exception) {\n                Log.w(\"ProxyVpnService\", \"Error processing proxyPassDomain: $domain\", e)\n            }\n        }\n    }\n\n    /**\n     * 添加 CIDR 格式的排除路由\n     * @param builder VPN Builder 实例\n     * @param cidr CIDR 格式的地址，如 \"192.168.0.0/16\"\n     */\n    private fun addCidrExcludeRoute(builder: Builder, cidr: String) {\n        try {\n            val parts = cidr.split(\"/\")\n            if (parts.size != 2) {\n                Log.w(\"ProxyVpnService\", \"Invalid CIDR format: $cidr\")\n                return\n            }\n\n            val ipAddress = parts[0]\n            val prefixLength = parts[1].toIntOrNull()\n\n            if (prefixLength == null || prefixLength !in 0..32) {\n                Log.w(\"ProxyVpnService\", \"Invalid prefix length in CIDR: $cidr\")\n                return\n            }\n\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                val inetAddress = InetAddress.getByName(ipAddress)\n                builder.excludeRoute(IpPrefix(inetAddress, prefixLength))\n                Log.d(\"ProxyVpnService\", \"Added excludeRoute: $cidr\")\n            }\n        } catch (e: Exception) {\n            Log.w(\"ProxyVpnService\", \"Failed to add CIDR excludeRoute: $cidr\", e)\n        }\n    }\n\n    /**\n     * 添加单个 IP 地址的排除路由\n     * @param builder VPN Builder 实例\n     * @param ipAddress IP 地址字符串\n     */\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    private fun addSingleIpExcludeRoute(builder: Builder, ipAddress: String) {\n        val inetAddress = InetAddress.getByName(ipAddress)\n        builder.excludeRoute(IpPrefix(inetAddress, 32))\n        Log.d(\"ProxyVpnService\", \"Added excludeRoute for single IP: $ipAddress/32\")\n    }\n\n    /**\n     * 检查字符串是否是有效的 IPv4 地址格式\n     * @param ip IP 地址字符串\n     * @return 是否是有效的 IPv4 地址\n     */\n    private fun isValidIpAddress(ip: String): Boolean {\n        return ip.matches(Regex(\"\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+\"))\n    }\n\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/VpnAlertDialog.kt",
    "content": "package com.network.proxy\n\nimport android.app.Activity\nimport android.app.AlertDialog\nimport android.os.Bundle\nimport kotlin.system.exitProcess\n\n/**\n * @author wanghongen\n */\nclass VpnAlertDialog : Activity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val dialog: AlertDialog = AlertDialog.Builder(this)\n            .setTitle(\"提示\")\n            .setMessage(\"必须添加VPN才能使用\")\n            .setPositiveButton(\"确认\") { _, _ ->\n                exitProcess(0)\n            }\n            .setCancelable(false)\n            .create()\n        dialog.show()\n    }\n\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/plugin/AndroidFlutterPlugin.kt",
    "content": "package com.network.proxy.plugin\n\nimport android.app.Activity\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\nimport io.flutter.embedding.engine.plugins.activity.ActivityAware\nimport io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding\n\nabstract class AndroidFlutterPlugin : FlutterPlugin, ActivityAware {\n\n    protected lateinit var activity: Activity\n\n    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {\n    }\n\n    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {\n    }\n\n    override fun onAttachedToActivity(binding: ActivityPluginBinding) {\n        activity = binding.activity\n    }\n\n    override fun onDetachedFromActivityForConfigChanges() {\n    }\n\n    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {\n    }\n\n    override fun onDetachedFromActivity() {\n    }\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/plugin/AppInfo.kt",
    "content": "package com.network.proxy.plugin\n\nimport android.content.pm.ApplicationInfo\nimport android.content.pm.PackageManager\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.drawable.BitmapDrawable\nimport android.graphics.drawable.Drawable\nimport java.io.ByteArrayOutputStream\nimport androidx.core.graphics.createBitmap\n\nclass ProcessInfo(name: CharSequence, packageName: String, icon: ByteArray?, versionName: String?) :\n    HashMap<String, Any?>() {\n    init {\n        put(\"name\", name)\n        put(\"packageName\", packageName)\n        put(\"icon\", icon)\n        put(\"versionName\", versionName)\n    }\n\n    fun copy(): ProcessInfo {\n        val name = this[\"name\"] as? CharSequence ?: \"\"\n        val packageName = this[\"packageName\"] as? String ?: \"\"\n        val icon = this[\"icon\"] as? ByteArray\n        val versionName = this[\"versionName\"] as? String\n        val newInfo = ProcessInfo(name, packageName, icon, versionName)\n        newInfo.putAll(this)\n        return newInfo\n    }\n\n    companion object {\n        fun create(\n            packageManager: PackageManager,\n            app: ApplicationInfo,\n            withIcon: Boolean = true\n        ): ProcessInfo {\n            val name = packageManager.getApplicationLabel(app)\n            val packageName = app.packageName\n            val icon =\n                if (withIcon) drawableToByteArray(app.loadIcon(packageManager)) else ByteArray(0)\n            val packageInfo = packageManager.getPackageInfo(app.packageName, 0)\n            // 部分应用可能没有设置versionName，将导致获取列表操作失败\n            val versionName = packageInfo.versionName ?: \"\"\n\n            return ProcessInfo(name, packageName, icon, versionName)\n        }\n\n        private fun drawableToByteArray(drawable: Drawable): ByteArray {\n            val bitmap = drawableToBitmap(drawable)\n            val stream = ByteArrayOutputStream()\n            bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)\n            return stream.toByteArray()\n        }\n\n        private fun drawableToBitmap(drawable: Drawable): Bitmap {\n            if (drawable is BitmapDrawable) {\n                return drawable.bitmap\n            }\n\n            // 获取宽度和高度，如果无效则使用默认值 96dp\n            var width = drawable.intrinsicWidth\n            var height = drawable.intrinsicHeight\n\n            // 如果宽度或高度无效（≤ 0），使用默认的 96 作为大小\n            if (width <= 0) width = 96\n            if (height <= 0) height = 96\n\n            val bitmap = createBitmap(width, height)\n            val canvas = Canvas(bitmap)\n            drawable.setBounds(0, 0, canvas.width, canvas.height)\n            drawable.draw(canvas)\n            return bitmap\n        }\n\n    }\n\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/plugin/AppLifecyclePlugin.kt",
    "content": "package com.network.proxy.plugin\n\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\nimport io.flutter.plugin.common.MethodChannel\n\nclass AppLifecyclePlugin : AndroidFlutterPlugin() {\n    var channel: MethodChannel? = null\n\n    companion object {\n        const val CHANNEL = \"com.proxy/appLifecycle\"\n\n    }\n\n    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {\n        channel = MethodChannel(binding.binaryMessenger, CHANNEL)\n    }\n\n    fun onUserLeaveHint() {\n        channel?.invokeMethod(\"onUserLeaveHint\", null)\n    }\n\n    fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {\n        channel?.invokeMethod(\"onPictureInPictureModeChanged\", isInPictureInPictureMode)\n    }\n\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/plugin/InstalledAppsPlugin.kt",
    "content": "package com.network.proxy.plugin\n\nimport android.content.pm.ApplicationInfo\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\nimport io.flutter.plugin.common.MethodChannel\nimport java.util.Locale\nimport java.util.concurrent.Callable\nimport java.util.concurrent.Executors\nimport java.util.concurrent.TimeUnit\n\n/**\n * 已经安装应用列表\n *\n * @author wanghongen\n */\nclass InstalledAppsPlugin : AndroidFlutterPlugin() {\n    var channel: MethodChannel? = null\n\n    companion object {\n        const val CHANNEL = \"com.proxy/installedApps\"\n    }\n\n    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {\n        channel = MethodChannel(binding.binaryMessenger, CHANNEL)\n\n        channel!!.setMethodCallHandler { call, result ->\n            when (call.method) {\n                \"getInstalledApps\" -> {\n                    val withIcon = call.argument<Boolean>(\"withIcon\") ?: false\n                    val packageNamePrefix = call.argument<String>(\"packageNamePrefix\") ?: \"\"\n                    val includeSystemApps = call.argument<Boolean>(\"includeSystemApps\") ?: false\n                    Thread {\n                        result.success(\n                            getInstalledApps(\n                                withIcon,\n                                packageNamePrefix,\n                                includeSystemApps\n                            )\n                        )\n                    }.start()\n                }\n\n                \"getAppInfo\" -> {\n                    val packageName = call.argument<String>(\"packageName\") ?: \"\"\n                    result.success(getAppInfo(packageName))\n                }\n\n                else -> result.notImplemented()\n            }\n        }\n    }\n\n    private fun getAppInfo(packageName: String): ProcessInfo {\n        val packageManager = activity.packageManager\n        packageManager.getApplicationInfo(packageName, 0).let { app ->\n            return ProcessInfo.create(packageManager, app, true)\n        }\n    }\n\n    private fun isSystemApp(applicationInfo: ApplicationInfo?): Boolean {\n        if (applicationInfo == null) return false\n        return (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0\n    }\n\n    private fun getInstalledApps(\n        withIcon: Boolean,\n        packageNamePrefix: String,\n        includeSystemApps: Boolean\n    ): List<ProcessInfo> {\n        val packageManager = activity.packageManager\n        var installedApps = packageManager.getInstalledApplications(0)\n\n        if (!includeSystemApps) {\n            installedApps =\n                installedApps.filter { app -> !isSystemApp(app) }\n        }\n\n        if (packageNamePrefix.isNotEmpty()) {\n            installedApps = installedApps.filter { app ->\n                app.packageName.startsWith(\n                    packageNamePrefix.lowercase(Locale.ENGLISH)\n                )\n            }\n        }\n\n        val threadPoolExecutor = Executors.newFixedThreadPool(4)\n        installedApps.map { app ->\n            val task: Callable<ProcessInfo> = Callable {\n                ProcessInfo.create(packageManager, app, withIcon)\n            }\n            threadPoolExecutor.submit(task)\n        }.map { future ->\n            future.get()\n        }.let {\n            threadPoolExecutor.shutdown()\n            threadPoolExecutor.awaitTermination(3, TimeUnit.SECONDS)\n            return it\n        }\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/plugin/PictureInPicturePlugin.kt",
    "content": "package com.network.proxy.plugin\n\nimport android.app.PendingIntent\nimport android.app.PictureInPictureParams\nimport android.app.RemoteAction\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.graphics.drawable.Icon\nimport android.os.Build\nimport android.util.Rational\nimport com.network.proxy.ProxyVpnService\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\nimport io.flutter.plugin.common.MethodChannel\nimport android.util.Log\nimport androidx.core.content.ContextCompat\n\n/**\n * 画中画插件\n */\nclass PictureInPicturePlugin : AndroidFlutterPlugin() {\n    private var registerBroadcast = false\n    var channel: MethodChannel? = null\n    var proxyHost: String? = null\n    var proxyPort: Int? = null\n    var allowApps: ArrayList<String>? = null\n    var disallowApps: ArrayList<String>? = null\n\n    ///广播事件接受者\n    private val vpnBroadcastReceiver = object : BroadcastReceiver() {\n        override fun onReceive(context: Context?, intent: Intent?) {\n            Log.d(\"com.network.proxy\", \"onReceive ${intent?.action}\")\n\n            if (context == null || (intent?.action != VPN_ACTION && intent?.action != CLEAN_ACTION)) {\n                return\n            }\n            if (intent.action == CLEAN_ACTION) {\n                channel?.invokeMethod(\"cleanSession\", null)\n                return\n            }\n\n            val isRunning = ProxyVpnService.isRunning\n\n            if (isRunning) {\n                activity.startService(ProxyVpnService.stopVpnIntent(activity))\n            } else {\n                val prepareVpn = ProxyVpnService.prepareVpn(activity, proxyHost!!, proxyPort!!, allowApps, disallowApps)\n                if (prepareVpn) {\n                    activity.startService(\n                        ProxyVpnService.startVpnIntent(\n                            activity,\n                            proxyHost,\n                            proxyPort,\n                            allowApps,\n                            disallowApps\n                        )\n                    )\n                }\n            }\n\n            //设置画中画参数\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                updatePictureInPictureParams(!isRunning)\n            }\n        }\n    }\n\n    companion object {\n        const val CHANNEL = \"com.proxy/pictureInPicture\"\n        const val VPN_ACTION = \"VPN_ACTION\"\n        const val CLEAN_ACTION = \"CLEAN_ACTION\"\n    }\n\n    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {\n        channel = MethodChannel(binding.binaryMessenger, CHANNEL)\n        channel!!.setMethodCallHandler { call, result ->\n            when (call.method) {\n                \"enterPictureInPictureMode\" -> {\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                        proxyHost = call.argument<String>(\"proxyHost\")\n                        proxyPort = call.argument<Int>(\"proxyPort\")\n                        allowApps = call.argument<ArrayList<String>>(\"allowApps\")\n                        disallowApps = call.argument<ArrayList<String>>(\"disallowApps\")\n\n                        val param = updatePictureInPictureParams(ProxyVpnService.isRunning)\n                        if (!registerBroadcast) {\n                            registerBroadcast = true\n                            ContextCompat.registerReceiver(\n                                activity,\n                                vpnBroadcastReceiver,\n                                IntentFilter().apply {\n                                    addAction(VPN_ACTION)\n                                    addAction(CLEAN_ACTION)\n                                },\n                                ContextCompat.RECEIVER_NOT_EXPORTED\n                            )\n                        }\n\n                        result.success(activity.enterPictureInPictureMode(param))\n                    }\n                }\n\n                else -> {\n                    result.notImplemented()\n                }\n            }\n        }\n    }\n\n    // 画中画参数\n    private fun updatePictureInPictureParams(isRunning: Boolean): PictureInPictureParams {\n\n        val params = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            PictureInPictureParams.Builder()\n                .setAspectRatio(Rational(9, 19))\n                .apply {\n                    setActions(actions(isRunning))   //vpn服务运行中，显示停止按钮\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n                        setSeamlessResizeEnabled(false)\n                    }\n                }\n                .build()\n        } else {\n            throw RuntimeException(\"getPictureInPictureParams error\")\n        }\n        activity.setPictureInPictureParams(params)\n        return params\n    }\n\n    //停止vpn服务 RemoteAction\n    private fun actions(isRunning: Boolean): List<RemoteAction> {\n        val pIntent: PendingIntent = PendingIntent.getBroadcast(\n            activity,\n            if (isRunning) 0 else 1,\n            Intent(VPN_ACTION).apply { setPackage(activity.packageName) },\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT\n        )\n\n        val cleanIntent: PendingIntent = PendingIntent.getBroadcast(\n            activity,\n            2,\n            Intent(CLEAN_ACTION).apply { setPackage(activity.packageName) },\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT\n        )\n\n        //vpn服务运行中，显示停止按钮\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            return listOf(\n                RemoteAction(\n                    Icon.createWithResource(\n                        this@PictureInPicturePlugin.activity,\n                        if (isRunning) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play\n                    ), \"Proxy\", \"Proxy\", pIntent\n                ),\n                RemoteAction(\n                    Icon.createWithResource(\n                        this@PictureInPicturePlugin.activity,\n                        android.R.drawable.ic_menu_delete\n                    ), \"Clean\", \"Clean\", cleanIntent\n                )\n            )\n        } else {\n            throw RuntimeException(\"action error\")\n        }\n    }\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/plugin/ProcessInfoPlugin.kt",
    "content": "package com.network.proxy.plugin\n\nimport com.network.proxy.vpn.util.ProcessInfoManager\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\nimport io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding\nimport io.flutter.plugin.common.MethodChannel\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n/**\n * 进程信息插件\n *\n * @author wanghongen\n */\nclass ProcessInfoPlugin : AndroidFlutterPlugin() {\n    private val processInfoManager = ProcessInfoManager.instance\n\n    companion object {\n        const val CHANNEL = \"com.proxy/processInfo\"\n    }\n\n    override fun onAttachedToActivity(binding: ActivityPluginBinding) {\n        super.onAttachedToActivity(binding)\n        processInfoManager.activity = binding.activity\n    }\n\n    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {\n        val channel = MethodChannel(binding.binaryMessenger, CHANNEL)\n        channel.setMethodCallHandler { call, result ->\n            when (call.method) {\n                \"getProcessByPort\" -> {\n                    val host = call.argument<String>(\"host\")\n                    val port = call.argument<Int>(\"port\")\n                    if (port != null) {\n                        CoroutineScope(Dispatchers.IO).launch {\n                            val appInfo = processInfoManager.getProcessInfoByPort(host, port)\n                            withContext(Dispatchers.Main) {\n                                result.success(appInfo)\n                            }\n                        }\n                    } else {\n                        result.error(\"INVALID_ARGUMENT\", \"Port is null\", null)\n                    }\n                }\n\n                \"getRemoteAddressByPort\" -> {\n                    val port = call.argument<Int>(\"port\")\n                    result.success(processInfoManager.getRemoteAddressByPort(port!!))\n                }\n\n                else -> {\n                    result.notImplemented()\n                }\n            }\n        }\n    }\n\n\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/plugin/VpnServicePlugin.kt",
    "content": "package com.network.proxy.plugin\n\nimport com.network.proxy.ProxyVpnService\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\nimport io.flutter.plugin.common.MethodChannel\n\nclass VpnServicePlugin : AndroidFlutterPlugin() {\n    companion object {\n        const val CHANNEL = \"com.proxy/proxyVpn\"\n        const val REQUEST_CODE: Int = 24\n    }\n\n    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {\n        val channel = MethodChannel(binding.binaryMessenger, CHANNEL)\n        channel.setMethodCallHandler { call, result ->\n            when (call.method) {\n                \"isRunning\" -> {\n                    result.success(ProxyVpnService.isRunning)\n                }\n\n                \"startVpn\" -> {\n                    val host = call.argument<String>(\"proxyHost\")\n                    val port = call.argument<Int>(\"proxyPort\")\n                    val allowApps = call.argument<ArrayList<String>>(\"allowApps\")\n                    val disallowApps = call.argument<ArrayList<String>>(\"disallowApps\")\n                    val setSystemProxy = call.argument<Boolean>(\"setSystemProxy\") ?: true\n                    val proxyPassDomains = call.argument<ArrayList<String>>(\"proxyPassDomains\")\n\n                    val prepareVpn = ProxyVpnService.prepareVpn(\n                        activity,\n                        host!!,\n                        port!!,\n                        allowApps,\n                        disallowApps,\n                        setSystemProxy,\n                        proxyPassDomains\n                    )\n                    if (prepareVpn) {\n                        startVpn(host, port, allowApps, disallowApps, setSystemProxy, proxyPassDomains)\n                    }\n                    result.success(prepareVpn)\n                }\n\n                \"stopVpn\" -> {\n                    stopVpn()\n                    result.success(null)\n                }\n\n                \"restartVpn\" -> {\n                    val host = call.argument<String>(\"proxyHost\")\n                    val port = call.argument<Int>(\"proxyPort\")\n                    val allowApps = call.argument<ArrayList<String>>(\"allowApps\")\n                    val disallowApps = call.argument<ArrayList<String>>(\"disallowApps\")\n                    val setSystemProxy = call.argument<Boolean>(\"setSystemProxy\") ?: true\n                    val proxyPassDomains = call.argument<ArrayList<String>>(\"proxyPassDomains\")\n\n                    stopVpn()\n                    startVpn(host!!, port!!, allowApps, disallowApps, setSystemProxy, proxyPassDomains)\n                    result.success(null)\n                }\n\n                else -> {\n                    result.notImplemented()\n                }\n            }\n        }\n    }\n\n    /**\n     * 启动vpn服务\n     */\n    private fun startVpn(\n        host: String,\n        port: Int,\n        allowApps: ArrayList<String>? = arrayListOf(),\n        disallowApps: ArrayList<String>? = arrayListOf(),\n        setSystemProxy: Boolean = true,\n        proxyPassDomains: ArrayList<String>? = null\n    ) {\n        val intent = ProxyVpnService.startVpnIntent(\n            activity,\n            host,\n            port,\n            allowApps,\n            disallowApps,\n            setSystemProxy,\n            proxyPassDomains\n        )\n        activity.startService(intent)\n    }\n\n    /**\n     * 停止vpn服务\n     */\n    private fun stopVpn() {\n        activity.startService(ProxyVpnService.stopVpnIntent(activity))\n    }\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/Connection.kt",
    "content": "package com.network.proxy.vpn\n\nimport android.util.Log\nimport com.network.proxy.vpn.socket.CloseableConnection\nimport com.network.proxy.vpn.transport.protocol.IP4Header\nimport com.network.proxy.vpn.transport.protocol.TCPHeader\nimport com.network.proxy.vpn.transport.protocol.UDPHeader\nimport com.network.proxy.vpn.util.PacketUtil\nimport java.io.ByteArrayOutputStream\nimport java.io.IOException\nimport java.nio.ByteBuffer\nimport java.nio.channels.SelectionKey\nimport java.nio.channels.spi.AbstractSelectableChannel\nimport kotlin.concurrent.Volatile\n\nclass Connection(\n    val protocol: Protocol,\n    val sourceIp: Int, val sourcePort: Int,\n    val destinationIp: Int, val destinationPort: Int,\n    private val connectionCloser: CloseableConnection\n) {\n\n    var channel: AbstractSelectableChannel? = null\n    var selectionKey: SelectionKey? = null\n\n    //接收用于存储来自远程主机的数据的缓冲器\n    private val receivingStream: ByteArrayOutputStream = ByteArrayOutputStream()\n\n    //发送缓冲区，用于存储要从vpn客户端发送到目标主机的数据\n    private val sendingStream: ByteArrayOutputStream = ByteArrayOutputStream()\n\n    var hasReceivedLastSegment = false\n\n    /**\n     * 是否初始化链接 针对代理判断协议延迟初始化\n     */\n    var isInitConnect = false\n\n    //指示三向握手是否已完成\n    var isConnected = false\n\n    //从客户端接收的最后一个数据包\n    var lastIpHeader: IP4Header? = null\n    var lastTcpHeader: TCPHeader? = null\n    var lastUdpHeader: UDPHeader? = null\n\n    var timestampSender = 0\n    var timestampReplyTo = 0\n\n    //从客户端接收的序列\n    var recSequence: Long = 0\n\n    //在tcp选项内的SYN期间由客户端发送\n    var maxSegmentSize = 0\n\n    //跟踪我们发送给客户端的ack，并等待客户端返回ack\n    var sendUnAck: Long = 0\n\n    //发送到客户端的下一个ack\n    var sendNext: Long = 0\n\n    //true when connection is about to be close\n    var isClosingConnection = false\n\n    //指示客户端的数据已准备好发送到目标\n    @Volatile\n    var isDataForSendingReady = false\n\n    //closing session and aborting connection, will be done by background task\n    @Volatile\n    var isAbortingConnection = false\n\n    //indicate that vpn client has sent FIN flag and it has been acked\n    var isAckedToFin = false\n\n    companion object {\n        fun getConnectionKey(\n            protocol: Protocol, destIp: Int, destPort: Int, sourceIp: Int, sourcePort: Int\n        ): String {\n            return protocol.name + \"|\" + PacketUtil.intToIPAddress(sourceIp) + \":\" + sourcePort +\n                    \"->\" + PacketUtil.intToIPAddress(destIp) + \":\" + destPort\n        }\n    }\n\n//    fun getConnectionKey(): String {\n//        return getConnectionKey(protocol, destinationIp, destinationIp, sourceIp, sourcePort)\n//    }\n\n    fun closeConnection() {\n        connectionCloser.closeConnection(this)\n    }\n\n    /**\n     * 设置要发送到目标服务器的数据\n     */\n    @Synchronized\n    fun setSendingData(data: ByteBuffer): Int {\n        val remaining = data.remaining()\n        sendingStream.write(data.array(), data.position(), data.remaining())\n        return remaining\n    }\n\n    @Synchronized\n    fun addReceivedData(data: ByteArray?) {\n        try {\n            receivingStream.write(data)\n        } catch (e: IOException) {\n            Log.e(TAG, e.toString())\n        }\n    }\n\n    /**\n     * 获取缓冲区中接收到的所有数据并清空它。\n     */\n    @Synchronized\n    fun getReceivedData(maxSize: Int): ByteArray? {\n        var data = receivingStream.toByteArray()\n        receivingStream.reset()\n        if (data.size > maxSize) {\n            val small = ByteArray(maxSize)\n            System.arraycopy(data, 0, small, 0, maxSize)\n            val len = data.size - maxSize\n            receivingStream.write(data, maxSize, len)\n            data = small\n        }\n        return data\n    }\n\n    /**\n     * buffer has more data for vpn client\n     */\n    fun hasReceivedData(): Boolean {\n        return receivingStream.size() > 0\n    }\n\n    fun hasDataToSend(): Boolean {\n        return sendingStream.size() > 0\n    }\n\n    /**\n     * 出列数据以发送到服务器\n     */\n    @Synchronized\n    fun getSendingData(): ByteArray? {\n        val data = sendingStream.toByteArray()\n        sendingStream.reset()\n        return data\n    }\n\n    fun cancelKey() {\n        selectionKey?.let {\n            synchronized(it) {\n                if (!it.isValid) return\n                it.cancel()\n            }\n        }\n\n    }\n\n    fun subscribeKey(op: Int) {\n        selectionKey?.let {\n            synchronized(it) {\n                if (!it.isValid) return\n                it.interestOps(it.interestOps() or op)\n            }\n        }\n    }\n\n    fun unsubscribeKey(op: Int) {\n        selectionKey?.let {\n            synchronized(it) {\n                if (!it.isValid) return\n                it.interestOps(it.interestOps() and op.inv())\n            }\n        }\n    }\n\n    override fun toString(): String {\n       return \"Connection{\" +\n                    \"protocol=\" + protocol +\n                    \", sourceIp=\" + PacketUtil.intToIPAddress(sourceIp) +\n                    \", sourcePort=\" + sourcePort +\n                    \", destinationIp=\" + PacketUtil.intToIPAddress(destinationIp) +\n                    \", destinationPort=\" + destinationPort +\n                    '}'\n    }\n\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/ConnectionHandler.kt",
    "content": "package com.network.proxy.vpn\n\nimport android.util.Log\nimport com.network.proxy.vpn.Connection.Companion.getConnectionKey\nimport com.network.proxy.vpn.socket.ClientPacketWriter\nimport com.network.proxy.vpn.socket.SocketNIODataService\nimport com.network.proxy.vpn.transport.icmp.ICMPPacket\nimport com.network.proxy.vpn.transport.icmp.ICMPPacketFactory\nimport com.network.proxy.vpn.transport.protocol.IP4Header\nimport com.network.proxy.vpn.transport.protocol.IPPacketFactory\nimport com.network.proxy.vpn.transport.protocol.TCPHeader\nimport com.network.proxy.vpn.transport.protocol.TCPPacketFactory\nimport com.network.proxy.vpn.transport.protocol.UDPPacketFactory\nimport com.network.proxy.vpn.util.PacketUtil.getOutput\nimport com.network.proxy.vpn.util.PacketUtil.intToIPAddress\nimport com.network.proxy.vpn.util.PacketUtil.isPacketCorrupted\nimport com.network.proxy.vpn.util.ProcessInfoManager\nimport com.network.proxy.vpn.util.TLS.isTLSClientHello\nimport java.io.IOException\nimport java.net.InetAddress\nimport java.net.InetSocketAddress\nimport java.nio.ByteBuffer\nimport java.nio.channels.SelectionKey\nimport java.nio.channels.SocketChannel\nimport java.util.concurrent.ExecutorService\nimport java.util.concurrent.SynchronousQueue\nimport java.util.concurrent.ThreadPoolExecutor\nimport java.util.concurrent.TimeUnit\n\nclass ConnectionHandler(\n    private val manager: ConnectionManager,\n    private val nioService: SocketNIODataService,\n    private val writer: ClientPacketWriter\n) {\n\n    private val pingThreadPool: ExecutorService = ThreadPoolExecutor(\n        1, 20,  // 1 - 20 parallel pings max\n        60L, TimeUnit.SECONDS,\n        SynchronousQueue(),\n        ThreadPoolExecutor.DiscardPolicy() // Replace running pings if there's too many\n    )\n\n    /**\n     * Handle unknown raw IP packet data\n     *\n     * @param stream ByteBuffer to be read\n     */\n    @Throws(IOException::class)\n    fun handlePacket(stream: ByteBuffer) {\n        stream.rewind()\n\n        val ipHeader = IPPacketFactory.createIP4Header(stream)\n\n        if (ipHeader == null) {\n            stream.rewind()\n            Log.w(TAG, \"Malformed IP packet \")\n            return\n        }\n        if (ipHeader.protocol.toInt() == 6) {\n            handleTCPPacket(stream, ipHeader)\n        } else if (ipHeader.protocol.toInt() == 17) {\n            handleUDPPacket(stream, ipHeader)\n        } else if (ipHeader.protocol.toInt() == 1) {\n            handleICMPPacket(stream, ipHeader)\n        } else {\n            Log.w(TAG, \"Unsupported IP protocol: \" + ipHeader.protocol)\n        }\n    }\n\n    @Throws(IOException::class)\n    private fun handleUDPPacket(clientPacketData: ByteBuffer, ipHeader: IP4Header) {\n        val udpHeader = UDPPacketFactory.createUDPHeader(clientPacketData)\n        var connection = manager.getConnection(\n            Protocol.UDP,\n            ipHeader.destinationIP, udpHeader.destinationPort,\n            ipHeader.sourceIP, udpHeader.sourcePort\n        )\n        val newSession = connection == null\n        if (connection == null) {\n            connection = manager.createUDPConnection(\n                ipHeader.destinationIP, udpHeader.destinationPort,\n                ipHeader.sourceIP, udpHeader.sourcePort\n            )\n        }\n        synchronized(connection) {\n            connection.lastIpHeader = ipHeader\n            connection.lastUdpHeader = udpHeader\n            manager.addClientData(clientPacketData, connection)\n            connection.isDataForSendingReady = true\n\n            // We don't register the session until it's fully populated (as above)\n            if (newSession) nioService.registerSession(connection)\n\n            // Ping the NIO thread to write this, when the session is next writable\n            connection.subscribeKey(SelectionKey.OP_WRITE)\n            nioService.refreshSelect(connection)\n        }\n        manager.keepSessionAlive(connection)\n    }\n\n    /**\n     * 是否支持协议\n     */\n    private val methods: List<String> =\n        mutableListOf(\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"HEAD\", \"OPTIONS\", \"TRACE\", \"CONNECT\", \"PROPFIND\", \"REPORT\")\n\n    private fun supperProtocol(packetData: ByteBuffer): Boolean {\n        val position = packetData.position()\n        //判断是否是ssl握手\n        if (isTLSClientHello(packetData)) {\n            packetData.position(position)\n            return true\n        }\n        packetData.position(position)\n        for (method in methods) {\n            if (packetData.remaining() < method.length) {\n                continue\n            }\n            val bytes = ByteArray(method.length)\n            for (i in bytes.indices) {\n                bytes[i] = packetData[position + i]\n            }\n            if (method.equals(String(bytes), ignoreCase = true)) {\n                return true\n            }\n        }\n        return false\n    }\n\n    /**\n     * 获取代理地址\n     */\n    private fun getProxyAddress(\n        packetData: ByteBuffer, destinationIP: Int, destinationPort: Int\n    ): InetSocketAddress {\n        val ips = intToIPAddress(destinationIP)\n\n        // 检查是否在代理过滤列表中\n        if (shouldBypassProxy(ips)) {\n            Log.d(TAG, \"Bypassing proxy for $ips (in proxyPassDomains)\")\n            return InetSocketAddress(ips, destinationPort)\n        }\n\n        val supperProtocol = supperProtocol(packetData)\n        var socketAddress: InetSocketAddress? = null\n        if (supperProtocol) {\n            socketAddress = manager.proxyAddress\n        }\n        if (socketAddress == null) {\n            socketAddress = InetSocketAddress(ips, destinationPort)\n        }\n        return socketAddress\n    }\n\n    /**\n     * 检查是否应该绕过代理\n     * 支持 CIDR 格式（如 192.168.0.0/16）、IP地址、localhost 和域名（带通配符）匹配\n     */\n    private fun shouldBypassProxy(destinationIP: String): Boolean {\n        val proxyPassDomains = manager.proxyPassDomains ?: return false\n\n        for (domain in proxyPassDomains) {\n            try {\n                val trimmedDomain = domain.trim()\n\n                // 处理 localhost\n                if (trimmedDomain == \"localhost\" && (destinationIP == \"127.0.0.1\" || destinationIP == \"localhost\")) {\n                    return true\n                }\n\n                // 处理 CIDR 格式，如 192.168.0.0/16\n                if (trimmedDomain.contains(\"/\")) {\n                    if (matchesCIDR(destinationIP, trimmedDomain)) {\n                        return true\n                    }\n                } else if (trimmedDomain.startsWith(\"*.\")) {\n                    // 支持通配符匹配，如 *.example.com\n                    val suffix = trimmedDomain.substring(1) // 去掉 *\n                    if (destinationIP.endsWith(suffix)) {\n                        return true\n                    }\n                } else if (trimmedDomain.contains(\"*\")) {\n                    // 支持其他通配符模式\n                    val pattern = trimmedDomain.replace(\".\", \"\\\\.\").replace(\"*\", \".*\")\n                    if (destinationIP.matches(Regex(pattern))) {\n                        return true\n                    }\n                } else {\n                    // 精确匹配 IP 或域名\n                    if (destinationIP == trimmedDomain) {\n                        return true\n                    }\n\n                    // 尝试解析域名为IP地址进行比较\n                    try {\n                        val address = InetAddress.getByName(trimmedDomain)\n                        if (address.hostAddress == destinationIP) {\n                            return true\n                        }\n                    } catch (e: Exception) {\n                        Log.w(TAG, \"Error resolving domain $trimmedDomain: ${e.message}\")\n                    }\n                }\n            } catch (e: Exception) {\n                Log.w(TAG, \"Error matching domain $domain: ${e.message}\")\n            }\n        }\n\n        return false\n    }\n\n    /**\n     * 检查 IP 地址是否匹配 CIDR 格式\n     * @param ip 目标 IP 地址，如 \"192.168.1.1\"\n     * @param cidr CIDR 格式，如 \"192.168.0.0/16\"\n     * @return 是否匹配\n     */\n    private fun matchesCIDR(ip: String, cidr: String): Boolean {\n        try {\n            val parts = cidr.split(\"/\")\n            if (parts.size != 2) return false\n\n            val networkAddress = parts[0]\n            val prefixLength = parts[1].toIntOrNull() ?: return false\n\n            val ipBytes = ipToBytes(ip)\n            val networkBytes = ipToBytes(networkAddress)\n\n            if (ipBytes == null || networkBytes == null) return false\n\n            // 计算掩码\n            val mask = (-1L shl (32 - prefixLength)).toInt()\n\n            // 将 IP 地址转换为整数进行比较\n            val ipInt = bytesToInt(ipBytes)\n            val networkInt = bytesToInt(networkBytes)\n\n            return (ipInt and mask) == (networkInt and mask)\n        } catch (e: Exception) {\n            Log.w(TAG, \"Error matching CIDR $cidr for IP $ip: ${e.message}\")\n            return false\n        }\n    }\n\n    /**\n     * 将 IP 地址字符串转换为字节数组\n     */\n    private fun ipToBytes(ip: String): ByteArray? {\n        try {\n            val parts = ip.split(\".\")\n            if (parts.size != 4) return null\n\n            return ByteArray(4) { i ->\n                parts[i].toInt().toByte()\n            }\n        } catch (_: Exception) {\n            return null\n        }\n    }\n\n    /**\n     * 将字节数组转换为整数\n     */\n    private fun bytesToInt(bytes: ByteArray): Int {\n        return ((bytes[0].toInt() and 0xFF) shl 24) or\n               ((bytes[1].toInt() and 0xFF) shl 16) or\n               ((bytes[2].toInt() and 0xFF) shl 8) or\n               (bytes[3].toInt() and 0xFF)\n    }\n\n    @Throws(IOException::class)\n    private fun handleTCPPacket(clientPacketData: ByteBuffer, ip4Header: IP4Header) {\n        val tcpHeader = TCPPacketFactory.createTCPHeader(clientPacketData)\n        val dataLength = clientPacketData.limit() - clientPacketData.position()\n        val sourceIP = ip4Header.sourceIP\n        val destinationIP = ip4Header.destinationIP\n        val sourcePort = tcpHeader.getSourcePort()\n        val destinationPort = tcpHeader.getDestinationPort()\n        if (tcpHeader.isSYN()) {\n            // 3-way handshake + create new session\n            replySynAck(ip4Header, tcpHeader)\n        } else if (tcpHeader.isACK()) {\n            val key =\n                getConnectionKey(Protocol.TCP, destinationIP, destinationPort, sourceIP, sourcePort)\n            val connection = manager.getConnectionByKey(key)\n            if (connection == null) {\n                Log.w(TAG, \"Ack for unknown session: $key\")\n                if (tcpHeader.isFIN()) {\n                    sendLastAck(ip4Header, tcpHeader)\n                } else if (!tcpHeader.isRST()) {\n                    sendRstPacket(ip4Header, tcpHeader, dataLength)\n                }\n                return\n            }\n            synchronized(connection) {\n                connection.lastIpHeader = ip4Header\n                connection.lastTcpHeader = tcpHeader\n\n                //any data from client?\n                if (dataLength > 0) {\n                    //init proxy\n                    initProxyConnect(clientPacketData, destinationIP, destinationPort, connection)\n\n                    //accumulate data from client\n                    if (connection.recSequence == 0L || tcpHeader.sequenceNumber >= connection.recSequence) {\n                        val addedLength = manager.addClientData(clientPacketData, connection)\n                        //send ack to client only if new data was added\n                        sendAck(ip4Header, tcpHeader, addedLength, connection)\n                    } else {\n                        sendAckForDisorder(ip4Header, tcpHeader, dataLength)\n                    }\n                } else {\n                    //an ack from client for previously sent data\n                    acceptAck(tcpHeader, connection)\n                    if (connection.isClosingConnection) {\n                        sendFinAck(ip4Header, tcpHeader, connection)\n                    } else if (connection.isAckedToFin && !tcpHeader.isFIN()) {\n                        //the last ACK from client after FIN-ACK flag was sent\n                        manager.closeConnection(\n                            Protocol.TCP,\n                            destinationIP,\n                            destinationPort,\n                            sourceIP,\n                            sourcePort\n                        )\n                        //\t\t\t\t\t\tLog.d(TAG, \"got last ACK after FIN, session is now closed.\");\n                    }\n                }\n                //received the last segment of data from vpn client\n                if (tcpHeader.isPSH()) {\n                    // Tell the NIO thread to immediately send data to the destination\n                    pushDataToDestination(connection, tcpHeader)\n                } else if (tcpHeader.isFIN()) {\n                    //fin from vpn client is the last packet\n                    //ack it\n//\t\t\t\t\tLog.d(TAG, \"FIN from vpn client, will ack it.\");\n                    ackFinAck(ip4Header, tcpHeader, connection)\n                } else if (tcpHeader.isRST()) {\n                    resetTCPConnection(ip4Header, tcpHeader)\n                }\n                if (!connection.isAbortingConnection) {\n                    manager.keepSessionAlive(connection)\n                }\n            }\n        } else if (tcpHeader.isFIN()) {\n            //case client sent FIN without ACK\n            val connection = manager.getConnection(\n                Protocol.TCP,\n                destinationIP,\n                destinationPort,\n                sourceIP,\n                sourcePort\n            )\n            if (connection == null) ackFinAck(\n                ip4Header,\n                tcpHeader,\n                null\n            ) else manager.keepSessionAlive(connection)\n        } else if (tcpHeader.isRST()) {\n            resetTCPConnection(ip4Header, tcpHeader)\n        } else {\n            Log.d(TAG, \"unknown TCP flag\")\n            val str1 = getOutput(ip4Header, tcpHeader, clientPacketData.array())\n            Log.d(TAG, \">>>>>>>> Received from client <<<<<<<<<<\")\n            Log.d(TAG, str1)\n            Log.d(TAG, \">>>>>>>>>>>>>>>>>>>end receiving from client>>>>>>>>>>>>>>>>>>>>>\")\n        }\n    }\n\n    private fun initProxyConnect(\n        clientPacketData: ByteBuffer, destinationIP: Int, destinationPort: Int,\n        connection: Connection\n    ) {\n        if (connection.isInitConnect) {\n            return\n        }\n\n        connection.isInitConnect = true\n        val proxyAddress =\n            getProxyAddress(clientPacketData, destinationIP, destinationPort)\n        try {\n            val channel = connection.channel as SocketChannel?\n            val connected = channel!!.connect(proxyAddress)\n            connection.isConnected = connected\n            nioService.registerSession(connection)\n\n            if (proxyAddress == manager.proxyAddress) {\n                //获取进程信息\n                ProcessInfoManager.instance.setConnectionOwnerUid(connection)\n                Log.d(\n                    TAG,\n                    \"Proxy Initiate connecting key:\" + connection.toString() + \" \" + channel.localAddress + \" to remote tcp server: \" + channel.remoteAddress\n                )\n            }\n        } catch (e: Exception) {\n            val ips = intToIPAddress(destinationIP)\n            Log.w(TAG, \"Failed to reconnect to $ips:$destinationPort\", e)\n        }\n    }\n\n    private fun sendRstPacket(ip: IP4Header, tcp: TCPHeader, dataLength: Int) {\n        val data = TCPPacketFactory.createRstData(ip, tcp, dataLength)\n        writer.write(data)\n        Log.d(\n            TAG, \"Sent RST Packet to client with dest => \" +\n                    intToIPAddress(ip.destinationIP) + \":\" +\n                    tcp.getDestinationPort()\n        )\n    }\n\n    private fun sendLastAck(ip: IP4Header, tcp: TCPHeader) {\n        val data = TCPPacketFactory.createResponseAckData(ip, tcp, tcp.sequenceNumber + 1)\n        writer.write(data)\n//\t\tLog.d(TAG,\"Sent last ACK Packet to client with dest => \" +\n//\t\t\t\tPacketUtil.intToIPAddress(ip.getDestinationIP()) + \":\" +\n//\t\t\t\ttcp.getDestinationPort());\n    }\n\n    private fun ackFinAck(ip: IP4Header, tcp: TCPHeader, connection: Connection?) {\n        val ack = tcp.sequenceNumber + 1\n        val seq = tcp.ackNumber\n        val data = TCPPacketFactory.createFinAckData(ip, tcp, ack, seq, isFin = true, isAck = true)\n        writer.write(data)\n        if (connection != null) {\n            connection.cancelKey()\n            manager.closeConnection(connection)\n            //\t\t\tLog.d(TAG,\"ACK to client's FIN and close session => \"+PacketUtil.intToIPAddress(ip.getDestinationIP())+\":\"+tcp.getDestinationPort()\n//\t\t\t\t\t+\"-\"+PacketUtil.intToIPAddress(ip.getSourceIP())+\":\"+tcp.getSourcePort());\n        }\n    }\n\n    private fun sendFinAck(ip: IP4Header, tcp: TCPHeader, connection: Connection) {\n        val ack = tcp.sequenceNumber\n        val seq = tcp.ackNumber\n        val data = TCPPacketFactory.createFinAckData(ip, tcp, ack, seq, isFin = true, isAck = false)\n        val stream = ByteBuffer.wrap(data)\n        writer.write(data)\n//        Log.d(TAG, \"00000000000 FIN-ACK packet data to vpn client 000000000000\")\n        var vpnIp: IP4Header? = null\n        try {\n            vpnIp = IPPacketFactory.createIP4Header(stream)\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n        var vpnTcp: TCPHeader? = null\n        try {\n            if (vpnIp != null) vpnTcp = TCPPacketFactory.createTCPHeader(stream)\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n        if (vpnIp != null && vpnTcp != null) {\n            val logOut = getOutput(vpnIp, vpnTcp, data)\n            Log.d(TAG, logOut)\n        }\n//        Log.d(TAG, \"0000000000000 finished sending FIN-ACK packet to vpn client 000000000000\")\n        connection.sendNext = seq + 1\n        //avoid re-sending it, from here client should take care the rest\n        connection.isClosingConnection = false\n    }\n\n    private fun pushDataToDestination(connection: Connection, tcp: TCPHeader) {\n        connection.isDataForSendingReady = true\n        connection.timestampReplyTo = tcp.timeStampSender\n        connection.timestampSender = System.currentTimeMillis().toInt()\n\n        // Ping the NIO thread to write this, when the session is next writable\n        connection.subscribeKey(SelectionKey.OP_WRITE)\n        nioService.refreshSelect(connection)\n    }\n\n    /**\n     * send acknowledgment packet to VPN client\n     *\n     * @param acceptedDataLength Data Length\n     */\n    private fun sendAck(\n        ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int, connection: Connection\n    ) {\n        val ackNumber = connection.recSequence + acceptedDataLength\n        connection.recSequence = ackNumber\n        val ackData = TCPPacketFactory.createResponseAckData(ipHeader, tcpHeader, ackNumber)\n        writer.write(ackData)\n    }\n\n    /**\n     * resend the last acknowledgment packet to VPN client, e.g. when an unexpected out of order\n     * packet arrives.\n     */\n    private fun resendAck(connection: Connection) {\n        val data = TCPPacketFactory.createResponseAckData(\n            connection.lastIpHeader!!,\n            connection.lastTcpHeader!!,\n            connection.recSequence\n        )\n        writer.write(data)\n    }\n\n    private fun sendAckForDisorder(\n        ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int\n    ) {\n        val ackNumber = tcpHeader.sequenceNumber + acceptedDataLength\n        Log.e(\n            TAG, \"sent disorder ack, ack# \" + tcpHeader.sequenceNumber +\n                    \" + \" + acceptedDataLength + \" = \" + ackNumber\n        )\n        val data = TCPPacketFactory.createResponseAckData(ipHeader, tcpHeader, ackNumber)\n        writer.write(data)\n    }\n\n    /**\n     * acknowledge a packet.\n     *\n     * @param tcpHeader TCP Header\n     */\n    private fun acceptAck(tcpHeader: TCPHeader, connection: Connection) {\n        val isCorrupted = isPacketCorrupted(tcpHeader)\n\n//        connection.setPacketCorrupted(isCorrupted);\n        if (isCorrupted) {\n            Log.e(TAG, \"prev packet was corrupted, last ack# \" + tcpHeader.ackNumber)\n        }\n        if (tcpHeader.ackNumber > connection.sendUnAck ||\n            tcpHeader.ackNumber == connection.sendNext\n        ) {\n//            connection.setAcked(true);\n            connection.sendUnAck = tcpHeader.ackNumber\n            connection.recSequence = tcpHeader.sequenceNumber\n            connection.timestampReplyTo = tcpHeader.timeStampSender\n            connection.timestampSender = System.currentTimeMillis().toInt()\n        } else {\n            Log.d(\n                TAG,\n                \"Not Accepting ack# \" + tcpHeader.ackNumber + \" , it should be: \" + connection.sendNext\n            )\n            Log.d(TAG, \"Prev sendUnAck: \" + connection.sendUnAck)\n            //            connection.setAcked(false);\n        }\n    }\n\n    /**\n     * set connection as aborting so that background worker will close it.\n     *\n     * @param ip  IP\n     * @param tcp TCP\n     */\n    private fun resetTCPConnection(ip: IP4Header, tcp: TCPHeader) {\n        val session = manager.getConnection(\n            Protocol.TCP,\n            ip.destinationIP, tcp.getDestinationPort(),\n            ip.sourceIP, tcp.getSourcePort()\n        )\n        if (session != null) {\n            synchronized(session) { session.isAbortingConnection = true }\n        }\n    }\n\n    /**\n     * create a new client's session and SYN-ACK packet data to respond to client\n     */\n    @Throws(IOException::class)\n    private fun replySynAck(ipHeader: IP4Header, tcpHeader: TCPHeader) {\n        ipHeader.identification = 0\n        val packet = TCPPacketFactory.createSynAckPacketData(ipHeader, tcpHeader)\n        val tcpTransport = packet.transportHeader as TCPHeader\n        val connection = manager.createTCPConnection(\n            ipHeader.destinationIP, tcpHeader.getDestinationPort(),\n            ipHeader.sourceIP, tcpHeader.getSourcePort()\n        )\n        if (connection.lastIpHeader != null) {\n            // We have an existing session for this connection! We've somehow received a SYN\n            // for an existing socket (or some kind of other race). We resend the last ACK\n            // for this session, rejecting this SYN. Not clear why this happens, but it can.\n            resendAck(connection)\n            return\n        }\n        synchronized(connection) {\n            connection.maxSegmentSize = tcpTransport.maxSegmentSize.toInt()\n            connection.sendUnAck = tcpTransport.sequenceNumber\n            connection.sendNext = tcpTransport.sequenceNumber + 1\n            //client initial sequence has been incremented by 1 and set to ack\n            connection.recSequence = tcpTransport.ackNumber\n            connection.lastIpHeader = ipHeader\n            connection.lastTcpHeader = tcpHeader\n            if (connection.isInitConnect) {\n                nioService.registerSession(connection)\n            }\n            writer.write(packet.buffer)\n        }\n    }\n\n    private fun handleICMPPacket(clientPacketData: ByteBuffer, ipHeader: IP4Header) {\n        val requestPacket = ICMPPacketFactory.parseICMPPacket(clientPacketData)\n//        Log.d(TAG, \"Got an ICMP ping packet, type $requestPacket\")\n        if (requestPacket.type == ICMPPacket.DESTINATION_UNREACHABLE_TYPE) {\n            // This is a packet from the phone, telling somebody that a destination is unreachable.\n            // Might be caused by issues on our end, but it's unclear what kind of issues. Regardless,\n            // we can't send ICMP messages ourselves or react usefully, so we drop these silently.\n            return\n        } else require(requestPacket.type == ICMPPacket.ECHO_REQUEST_TYPE) {\n            // We only actually support outgoing ping packets. Loudly drop anything else:\n            \"Unknown ICMP type (\" + requestPacket.type + \"). Only echo requests are supported\"\n        }\n        pingThreadPool.execute(object : Runnable {\n            override fun run() {\n                try {\n                    if (!isReachable(intToIPAddress(ipHeader.destinationIP))) {\n                        Log.d(TAG, \"Failed ping, ignoring\")\n                        return\n                    }\n                    val response = ICMPPacketFactory.buildSuccessPacket(requestPacket)\n\n                    // Flip the address\n                    val destination = ipHeader.destinationIP\n                    val source = ipHeader.sourceIP\n                    ipHeader.sourceIP = destination\n                    ipHeader.destinationIP = source\n                    val responseData = ICMPPacketFactory.packetToBuffer(ipHeader, response)\n                    Log.d(TAG, \"Successful ping response\")\n                    writer.write(responseData)\n                } catch (e: Exception) {\n                    Log.w(TAG, \"Handling ICMP failed with \" + e.message)\n                    return\n                }\n            }\n\n            private fun isReachable(ipAddress: String): Boolean {\n                return try {\n                    InetAddress.getByName(ipAddress).isReachable(10000)\n                } catch (_: IOException) {\n                    false\n                }\n            }\n        })\n    }\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/ConnectionManager.kt",
    "content": "package com.network.proxy.vpn\n\nimport android.os.Build\nimport android.util.Log\nimport com.network.proxy.vpn.socket.CloseableConnection\nimport com.network.proxy.vpn.socket.Constant\nimport com.network.proxy.vpn.socket.ProtectSocketHolder.Companion.protect\nimport com.network.proxy.vpn.util.PacketUtil\nimport com.network.proxy.vpn.util.ProcessInfoManager\nimport java.io.IOException\nimport java.net.InetSocketAddress\nimport java.net.SocketAddress\nimport java.nio.ByteBuffer\nimport java.nio.channels.DatagramChannel\nimport java.nio.channels.SocketChannel\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.ConcurrentMap\n\n/**\n * 管理VPN客户端的连接\n */\nclass ConnectionManager private constructor() : CloseableConnection {\n    //单例\n    companion object {\n        private const val TAG = \"ConnectionManager\"\n        val instance = ConnectionManager()\n    }\n\n    private val table: ConcurrentMap<String, Connection> = ConcurrentHashMap()\n    var proxyAddress: InetSocketAddress? = null\n    var proxyPassDomains: ArrayList<String>? = null\n\n    private val DEFAULT_PORTS: List<Int> = listOf(\n        80,  // HTTP\n        443,  // HTTPS\n        8080,  // Common local dev ports\n        8000, 8080, 8888, 9000 // Common local dev ports\n    )\n\n    override fun closeConnection(connection: Connection) {\n        closeConnection(\n            connection.protocol, connection.destinationIp, connection.destinationPort,\n            connection.sourceIp, connection.sourcePort\n        )\n    }\n\n    /**\n     * 从内存中删除连接，然后关闭套接字。\n     *\n     */\n    fun closeConnection(protocol: Protocol, ip: Int, port: Int, srcIp: Int, srcPort: Int) {\n        val key = Connection.getConnectionKey(protocol, ip, port, srcIp, srcPort)\n        val connection: Connection? = table.remove(key)\n        Log.d(TAG, \"close connection $key\")\n\n        connection?.let {\n            val channel = connection.channel\n            try {\n                channel?.close()\n            } catch (e: IOException) {\n                e.printStackTrace()\n            }\n        }\n    }\n\n    fun getConnection(\n        protocol: Protocol, ip: Int, port: Int, srcIp: Int, srcPort: Int\n    ): Connection? {\n        val key = Connection.getConnectionKey(protocol, ip, port, srcIp, srcPort)\n        return getConnectionByKey(key)\n    }\n\n    fun getConnectionByKey(key: String?): Connection? {\n        return table[key]\n    }\n\n    /**\n     * 创建tcp连接\n     */\n    fun createTCPConnection(ip: Int, port: Int, srcIp: Int, srcPort: Int): Connection {\n        val key = Connection.getConnectionKey(Protocol.TCP, ip, port, srcIp, srcPort)\n        val existingConnection: Connection? = table[key]\n        if (existingConnection != null) {\n            return existingConnection\n        }\n\n        val connection = Connection(Protocol.TCP, srcIp, srcPort, ip, port, this)\n\n        val channel: SocketChannel = SocketChannel.open()\n        channel.socket().keepAlive = true\n        channel.socket().tcpNoDelay = true\n        channel.socket().soTimeout = 0\n        channel.socket().receiveBufferSize = Constant.MAX_RECEIVE_BUFFER_SIZE\n        channel.configureBlocking(false)\n\n        Log.d(TAG, \"created new SocketChannel for $key\")\n\n        protect(channel.socket())\n\n        connection.channel = channel\n\n        var socketAddress: SocketAddress? = null\n//        if (DEFAULT_PORTS.contains(port)) {\n//            socketAddress = proxyAddress\n//        }\n\n        connection.isInitConnect = socketAddress != null\n\n        if (socketAddress != null) {\n            val connected = channel.connect(socketAddress)\n            connection.isConnected = connected\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n                //获取进程信息\n                ProcessInfoManager.instance.setConnectionOwnerUid(connection)\n                Log.d(\n                    TAG,\n                    \"Initiate connecting  \" + channel.localAddress + \" to remote tcp server: \" + channel.remoteAddress\n                )\n            }\n        }\n\n        table[key] = connection\n        return connection\n    }\n\n\n    @Throws(IOException::class)\n    fun createUDPConnection(ip: Int, port: Int, srcIp: Int, srcPort: Int): Connection {\n        val keys = Connection.getConnectionKey(Protocol.UDP, ip, port, srcIp, srcPort)\n\n        val existingConnection: Connection? = table[keys]\n        if (existingConnection != null) return existingConnection\n\n        val connection = Connection(Protocol.UDP, srcIp, srcPort, ip, port, this)\n        val channel: DatagramChannel = DatagramChannel.open()\n        channel.socket().soTimeout = 0\n        channel.configureBlocking(false)\n        protect(channel.socket())\n        connection.channel = channel\n\n        // Initiate connection early to reduce latency\n        val ips = PacketUtil.intToIPAddress(ip)\n        val socketAddress: SocketAddress = InetSocketAddress(ips, port)\n        channel.connect(socketAddress)\n        connection.isConnected = channel.isConnected\n        table[keys] = connection\n\n        return connection\n    }\n\n    /**\n     * 添加来自客户端的数据，该数据稍后将在接收到PSH标志时发送到目的服务器。\n     */\n    fun addClientData(buffer: ByteBuffer, session: Connection): Int {\n        return if (buffer.limit() <= buffer.position()) 0 else session.setSendingData(buffer)\n    }\n\n    /**\n     * 阻止java垃圾收集器收集会话\n     */\n    fun keepSessionAlive(connection: Connection) {\n        val key = Connection.getConnectionKey(\n            connection.protocol, connection.destinationIp, connection.destinationPort,\n            connection.sourceIp, connection.sourcePort\n        )\n        table[key] = connection\n    }\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/Protocol.java",
    "content": "package com.network.proxy.vpn;\n\npublic enum Protocol {\n    TCP,\n    UDP\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/ProxyVpnThread.kt",
    "content": "package com.network.proxy.vpn\n\nimport android.os.ParcelFileDescriptor\nimport android.util.Log\nimport com.network.proxy.ProxyVpnService.Companion.MAX_PACKET_LEN\nimport com.network.proxy.vpn.socket.ClientPacketWriter\nimport com.network.proxy.vpn.socket.SocketNIODataService\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\nimport java.io.InterruptedIOException\nimport java.net.InetSocketAddress\nimport java.nio.ByteBuffer\n\n\n/**\n * VPN线程，负责处理VPN接收到的数据包\n * @author wanghongen\n */\nclass ProxyVpnThread(\n    vpnInterface: ParcelFileDescriptor,\n    proxyHost: String,\n    proxyPort: Int,\n    proxyPassDomains: ArrayList<String>? = null,\n) : Thread(\"Vpn thread\") {\n    companion object {\n        const val TAG = \"ProxyVpnThread\"\n    }\n\n    @Volatile\n    private var running = false\n\n    private val vpnReadChannel = FileInputStream(vpnInterface.fileDescriptor).channel\n\n    // 此VPN接收的来自上游服务器的数据包\n    private val vpnWriteStream = FileOutputStream(vpnInterface.fileDescriptor)\n    private val vpnPacketWriter = ClientPacketWriter(vpnWriteStream)\n    private val vpnPacketWriterThread = Thread(vpnPacketWriter)\n\n    // Background service & task for non-blocking socket\n    private val nioService = SocketNIODataService(vpnPacketWriter)\n    private val dataServiceThread = Thread(nioService, \"Socket NIO thread\")\n\n    private val manager = ConnectionManager.instance.apply {\n        //流量转发到代理地址\n        this.proxyAddress = InetSocketAddress(proxyHost, proxyPort)\n        this.proxyPassDomains = proxyPassDomains\n    }\n\n    private val handler = ConnectionHandler(manager, nioService, vpnPacketWriter)\n\n    private var currentThread: Thread? = null\n\n    override fun run() {\n        Log.i(TAG, \"Vpn thread starting\")\n        currentThread = currentThread()\n        dataServiceThread.start()\n        vpnPacketWriterThread.start()\n\n        val readBuffer = ByteBuffer.allocate(MAX_PACKET_LEN)\n        running = true\n        while (running) {\n            try {\n                val length = vpnReadChannel.read(readBuffer)\n\n                if (length > 0) {\n                    try {\n                        readBuffer.flip()\n                        handler.handlePacket(readBuffer)\n                    } catch (e: Exception) {\n                        val errorMessage = (e.message ?: e.toString())\n                        Log.e(TAG, errorMessage, e)\n                    }\n\n                    readBuffer.clear()\n                } else {\n                    sleep(50)\n                }\n            } catch (e: InterruptedException) {\n                Log.i(TAG, \"Sleep interrupted: \" + e.message)\n            } catch (e: InterruptedIOException) {\n                Log.i(TAG, \"Read interrupted: \" + e.message)\n            } catch (e: Exception) {\n                val errorMessage = (e.message ?: e.toString())\n                Log.e(TAG, errorMessage, e)\n                if (!vpnReadChannel.isOpen) {\n                    Log.i(TAG, \"VPN read channel closed\")\n                    running = false\n                }\n            }\n        }\n\n        Log.i(TAG, \"Vpn thread stop\")\n    }\n\n    @Synchronized\n    fun stopThread() {\n        if (running) {\n            running = false\n            nioService.shutdown()\n            dataServiceThread.interrupt()\n\n            vpnPacketWriter.shutdown()\n            vpnPacketWriterThread.interrupt()\n            currentThread?.interrupt()\n        }\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/Tag.kt",
    "content": "package com.network.proxy.vpn\n\n\nfun formatTag(tag: String): String {\n    return tag\n}\n\nval Any.TAG: String\n    get() {\n        return javaClass.name\n    }\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/socket/ClientPacketWriter.kt",
    "content": "package com.network.proxy.vpn.socket\n\nimport android.util.Log\nimport java.io.FileOutputStream\nimport java.io.IOException\nimport java.util.concurrent.BlockingDeque\nimport java.util.concurrent.LinkedBlockingDeque\nimport kotlin.concurrent.Volatile\n\nclass ClientPacketWriter(private val clientWriter: FileOutputStream) : Runnable {\n    companion object {\n        private const val TAG: String = \"ClientPacketWriter\"\n        private const val MAX_PACKET_LEN = 32767\n    }\n\n    @Volatile\n    private var shutdown = false\n\n    private val packetQueue: BlockingDeque<ByteArray> = LinkedBlockingDeque()\n\n    fun write(data: ByteArray) {\n        if (data.size > MAX_PACKET_LEN) throw Error(\"Packet too large\")\n        packetQueue.addLast(data)\n    }\n\n    fun shutdown() {\n        this.shutdown = true\n    }\n\n    override fun run() {\n        while (!this.shutdown && clientWriter.channel.isOpen) {\n            try {\n                val data: ByteArray = this.packetQueue.take()\n                try {\n                    this.clientWriter.write(data)\n                } catch (e: IOException) {\n                    Log.e(TAG, \"Error writing $shutdown data.length bytes to the VPN\")\n                    e.printStackTrace()\n//                    this.packetQueue.addFirst(data) // Put the data back, so it's resent\n                    Thread.sleep(10) // Add an arbitrary tiny pause, in case that helps\n                }\n            } catch (ignored: InterruptedException) {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/socket/CloseableConnection.kt",
    "content": "package com.network.proxy.vpn.socket\n\nimport com.network.proxy.vpn.Connection\n\ninterface CloseableConnection {\n    /**\n     * 关闭连接\n     */\n    fun closeConnection(connection: Connection)\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/socket/Constant.kt",
    "content": "package com.network.proxy.vpn.socket\n\nobject Constant {\n    const val MAX_RECEIVE_BUFFER_SIZE = 65535\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/socket/ProtectSocket.kt",
    "content": "package com.network.proxy.vpn.socket\n\nimport java.net.DatagramSocket\nimport java.net.Socket\n\ninterface ProtectSocket {\n\n    /**\n     * 保护Socket不受VPN连接的影响。保护后，通过该套接字发送的数据将直接进入底层网络，因此其流量不会通过VPN转发。\n     */\n    fun protect(socket: Socket): Boolean\n\n    fun protect(socket: DatagramSocket): Boolean\n\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/socket/ProtectSocketHolder.kt",
    "content": "package com.network.proxy.vpn.socket\n\nimport java.net.DatagramSocket\nimport java.net.Socket\n\n/**\n * ProtectSocket的持有者，用于在VPNService中获取ProtectSocket的实例\n */\nclass ProtectSocketHolder {\n\n    companion object {\n        private var protectSocket: ProtectSocket? = null\n\n        fun setProtectSocket(protectSocket: ProtectSocket) {\n            this.protectSocket = protectSocket\n        }\n\n        fun getProtectSocket(): ProtectSocket? {\n            return protectSocket\n        }\n\n        fun protect(socket: Socket): Boolean {\n            return protectSocket?.protect(socket) ?: false\n        }\n\n        fun protect(socket: DatagramSocket): Boolean {\n            return protectSocket?.protect(socket) ?: false\n        }\n    }\n\n\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/socket/SocketChannelReader.java",
    "content": "package com.network.proxy.vpn.socket;\n\nimport androidx.annotation.NonNull;\n\nimport android.util.Log;\n\nimport com.network.proxy.vpn.Connection;\nimport com.network.proxy.vpn.TagKt;\nimport com.network.proxy.vpn.transport.protocol.IP4Header;\nimport com.network.proxy.vpn.transport.protocol.TCPHeader;\nimport com.network.proxy.vpn.transport.protocol.TCPPacketFactory;\nimport com.network.proxy.vpn.transport.protocol.UDPPacketFactory;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.ClosedByInterruptException;\nimport java.nio.channels.ClosedChannelException;\nimport java.nio.channels.DatagramChannel;\nimport java.nio.channels.NotYetConnectedException;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.SocketChannel;\nimport java.nio.channels.spi.AbstractSelectableChannel;\n\n\n/**\n * Takes a session, and reads all available upstream data back into it.\n * Used by the NIO thread, and run synchronously as part of that non-blocking loop.\n */\nclass SocketChannelReader {\n\n    private final String TAG = TagKt.getTAG(this);\n\n    private final ClientPacketWriter writer;\n\n    public SocketChannelReader(ClientPacketWriter writer) {\n        this.writer = writer;\n    }\n\n    public void read(Connection connection) {\n        AbstractSelectableChannel channel = connection.getChannel();\n\n        if (channel instanceof SocketChannel) {\n            readTCP(connection);\n        } else if (channel instanceof DatagramChannel) {\n            readUDP(connection);\n        } else {\n            return;\n        }\n\n        // Resubscribe to reads, so that we're triggered again if more data arrives later.\n        connection.subscribeKey(SelectionKey.OP_READ);\n\n        if (connection.isAbortingConnection()) {\n            Log.d(TAG, \"removing aborted connection -> \" + connection);\n            connection.cancelKey();\n            if (channel instanceof SocketChannel) {\n                try {\n                    SocketChannel socketChannel = (SocketChannel) channel;\n                    if (socketChannel.isConnected()) {\n                        socketChannel.close();\n                    }\n                } catch (IOException e) {\n                    Log.e(TAG, e.toString());\n                }\n            } else {\n                try {\n                    DatagramChannel datagramChannel = (DatagramChannel) channel;\n                    if (datagramChannel.isConnected()) {\n                        datagramChannel.close();\n                    }\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n            }\n            connection.closeConnection();\n        }\n    }\n\n    private void readTCP(@NonNull Connection connection) {\n        if (connection.isAbortingConnection()) {\n            return;\n        }\n\n        SocketChannel channel = (SocketChannel) connection.getChannel();\n        ByteBuffer buffer = ByteBuffer.allocate(Constant.MAX_RECEIVE_BUFFER_SIZE);\n        int len;\n\n        try {\n            do {\n                len = channel.read(buffer);\n                if (len > 0) { //-1 mean it reach the end of stream\n                    sendToRequester(buffer, len, connection);\n                    buffer.clear();\n                } else if (len == -1) {\n//\t\t\t\t\tLog.d(TAG,\"End of data from remote server, will send FIN to client\");\n                    Log.d(TAG, \"send FIN to: \" + connection);\n                    sendFin(connection);\n                    connection.setAbortingConnection(true);\n                }\n            } while (len > 0);\n        } catch (NotYetConnectedException e) {\n            Log.e(TAG, \"socket not connected\");\n        } catch (ClosedByInterruptException e) {\n            Log.e(TAG, \"ClosedByInterruptException reading SocketChannel: \" + e.getMessage());\n        } catch (ClosedChannelException e) {\n            Log.e(TAG, \"ClosedChannelException reading SocketChannel: \" + e.getMessage());\n        } catch (IOException e) {\n            Log.e(TAG, \"Error reading data from SocketChannel: \" + e.getMessage());\n            connection.setAbortingConnection(true);\n        }\n    }\n\n    private void sendToRequester(ByteBuffer buffer, int dataSize, @NonNull Connection connection) {\n        // Last piece of data is usually smaller than MAX_RECEIVE_BUFFER_SIZE. We use this as a\n        // trigger to set PSH on the resulting TCP packet that goes to the VPN.\n        connection.setHasReceivedLastSegment(dataSize < Constant.MAX_RECEIVE_BUFFER_SIZE);\n\n        buffer.limit(dataSize);\n        buffer.flip();\n        // TODO should allocate new byte array?\n        byte[] data = new byte[dataSize];\n        System.arraycopy(buffer.array(), 0, data, 0, dataSize);\n        connection.addReceivedData(data);\n        //pushing all data to vpn client\n        while (connection.hasReceivedData()) {\n            pushDataToClient(connection);\n        }\n    }\n\n    /**\n     * create packet data and send it to VPN client\n     */\n    private void pushDataToClient(@NonNull Connection connection) {\n        if (!connection.hasReceivedData()) {\n            //no data to send\n            Log.d(TAG, \"no data for vpn client\");\n        }\n\n        IP4Header ipHeader = connection.getLastIpHeader();\n        TCPHeader tcpheader = connection.getLastTcpHeader();\n        // TODO What does 60 mean?\n        int max = connection.getMaxSegmentSize() - 60;\n\n        if (max < 1) {\n            max = 1024;\n        }\n\n        byte[] packetBody = connection.getReceivedData(max);\n        if (packetBody != null && packetBody.length > 0) {\n            long unAck = connection.getSendNext();\n            long nextUnAck = connection.getSendNext() + packetBody.length;\n            connection.setSendNext((int) nextUnAck);\n            //we need this data later on for retransmission\n//            connection.setUnackData(packetBody);\n//            connection.setResendPacketCounter(0);\n\n            byte[] data = TCPPacketFactory.createResponsePacketData(ipHeader,\n                    tcpheader, packetBody, connection.getHasReceivedLastSegment(),\n                    connection.getRecSequence(), (int) unAck,\n                    connection.getTimestampSender(), connection.getTimestampReplyTo());\n\n            writer.write(data);\n        }\n    }\n\n    private void sendFin(Connection connection) {\n        final IP4Header ipHeader = connection.getLastIpHeader();\n        final TCPHeader tcpheader = connection.getLastTcpHeader();\n        final byte[] data = TCPPacketFactory.INSTANCE.createFinData(ipHeader, tcpheader,\n                connection.getRecSequence(), connection.getSendNext(),\n                connection.getTimestampSender(), connection.getTimestampReplyTo());\n\n        writer.write(data);\n    }\n\n    private void readUDP(Connection connection) {\n        DatagramChannel channel = (DatagramChannel) connection.getChannel();\n        ByteBuffer buffer = ByteBuffer.allocate(Constant.MAX_RECEIVE_BUFFER_SIZE);\n        int len;\n\n        try {\n            do {\n                if (connection.isAbortingConnection()) {\n                    break;\n                }\n\n                len = channel.read(buffer);\n                if (len > 0) {\n                    buffer.limit(len);\n                    buffer.flip();\n\n                    //create UDP packet\n                    byte[] data = new byte[len];\n                    System.arraycopy(buffer.array(), 0, data, 0, len);\n                    byte[] packetData = UDPPacketFactory.createResponsePacket(\n                            connection.getLastIpHeader(), connection.getLastUdpHeader(), data);\n\n                    //write to client\n                    writer.write(packetData);\n\n                    buffer.clear();\n                }\n            } while (len > 0);\n        } catch (NotYetConnectedException ex) {\n            Log.e(TAG, \"failed to read from unconnected UDP socket\");\n        } catch (IOException e) {\n            Log.e(TAG, \"Failed to read from UDP socket, aborting connection\");\n            connection.setAbortingConnection(true);\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/socket/SocketChannelWriter.java",
    "content": "package com.network.proxy.vpn.socket;\n\nimport androidx.annotation.NonNull;\nimport android.util.Log;\n\n\nimport com.network.proxy.vpn.Connection;\nimport com.network.proxy.vpn.TagKt;\nimport com.network.proxy.vpn.transport.protocol.TCPPacketFactory;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.DatagramChannel;\nimport java.nio.channels.NotYetConnectedException;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.SocketChannel;\nimport java.nio.channels.spi.AbstractSelectableChannel;\n\n\n/**\n * Takes a VPN session, and writes all received data from it to the upstream channel.\n * <p>\n * If any writes fail, it resubscribes to OP_WRITE, and tries again next time\n * that fires (as soon as the channel is ready for more data).\n * <p>\n * Used by the NIO thread, and run synchronously as part of that non-blocking loop.\n */\npublic class SocketChannelWriter {\n\tprivate final String TAG = TagKt.getTAG(this);\n\n\tprivate final ClientPacketWriter writer;\n\n\tSocketChannelWriter(ClientPacketWriter writer) {\n\t\tthis.writer = writer;\n\t}\n\n\tpublic void write(@NonNull Connection connection) {\n\t\tAbstractSelectableChannel channel = connection.getChannel();\n\t\tif (channel instanceof SocketChannel) {\n\t\t\twriteTCP(connection);\n\t\t} else if(channel instanceof DatagramChannel) {\n\t\t\twriteUDP(connection);\n\t\t} else {\n\t\t\t// We only ever create TCP & UDP channels, so this should never happen\n\t\t\tthrow new IllegalArgumentException(\"Unexpected channel type: \" + channel);\n\t\t}\n\n\t\tif (connection.isAbortingConnection()) {\n\t\t\tLog.d(TAG,\"removing aborted connection -> \" + connection);\n\t\t\tconnection.cancelKey();\n\n\t\t\tif (channel instanceof SocketChannel) {\n\t\t\t\ttry {\n\t\t\t\t\tSocketChannel socketChannel = (SocketChannel) channel;\n\t\t\t\t\tif (socketChannel.isConnected()) {\n\t\t\t\t\t\tsocketChannel.close();\n\t\t\t\t\t}\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\tDatagramChannel datagramChannel = (DatagramChannel) channel;\n\t\t\t\t\tif (datagramChannel.isConnected()) {\n\t\t\t\t\t\tdatagramChannel.close();\n\t\t\t\t\t}\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconnection.closeConnection();\n\t\t}\n\t}\n\n\tprivate void writeUDP(Connection connection) {\n\t\ttry {\n\t\t\twritePendingData(connection);\n//\t\t\tDate dt = new Date();\n//\t\t\tconnection.connectionStartTime = dt.getTime();\n\t\t}catch(NotYetConnectedException ex2){\n\t\t\tconnection.setAbortingConnection(true);\n\t\t\tLog.e(TAG,\"Error writing to unconnected-UDP server, will abort current connection: \"+ex2.getMessage());\n\t\t} catch (IOException e) {\n\t\t\tconnection.setAbortingConnection(true);\n\t\t\te.printStackTrace();\n\t\t\tLog.e(TAG,\"Error writing to UDP server, will abort connection: \"+e.getMessage());\n\t\t}\n\t}\n\t\n\tprivate void writeTCP(Connection connection) {\n\t\ttry {\n\t\t\twritePendingData(connection);\n\t\t} catch (NotYetConnectedException ex) {\n\t\t\tLog.e(TAG,\"failed to write to unconnected socket: \" + ex.getMessage());\n\t\t} catch (IOException e) {\n\t\t\tLog.e(TAG,\"Error writing to server: \" + e);\n\t\t\t\n\t\t\t//close connection with vpn client\n\t\t\tbyte[] rstData = TCPPacketFactory.INSTANCE.createRstData(\n\t\t\t\t\tconnection.getLastIpHeader(), connection.getLastTcpHeader(), 0);\n\n\t\t\twriter.write(rstData);\n\n\t\t\t//remove session\n\t\t\tLog.e(TAG,\"failed to write to remote socket, aborting connection\");\n\t\t\tconnection.setAbortingConnection(true);\n\t\t}\n\t}\n\n\tprivate void writePendingData(Connection connection) throws IOException {\n\t\tif (!connection.hasDataToSend()) return;\n\t\tAbstractSelectableChannel channel = connection.getChannel();\n\n\t\tbyte[] data = connection.getSendingData();\n\t\tByteBuffer buffer = ByteBuffer.allocate(data.length);\n\t\tbuffer.put(data);\n\t\tbuffer.flip();\n\n\t\twhile (buffer.hasRemaining()) {\n\t\t\tint bytesWritten = channel instanceof SocketChannel\n\t\t\t\t? ((SocketChannel) channel).write(buffer)\n\t\t\t\t: ((DatagramChannel) channel).write(buffer);\n\n\t\t\tif (bytesWritten == 0) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (buffer.hasRemaining()) {\n\t\t\t// The channel's own buffer is full, so we have to save this for later.\n\t\t\tLog.i(TAG, buffer.remaining() + \" bytes unwritten for \" + channel);\n\n\t\t\t// Put the remaining data from the buffer back into the session\n\t\t\tconnection.setSendingData(buffer.compact());\n\n\t\t\t// Subscribe to WRITE events, so we know when this is ready to resume.\n\t\t\tconnection.subscribeKey(SelectionKey.OP_WRITE);\n\t\t} else {\n\t\t\t// All done, all good -> wait until the next TCP PSH / UDP packet\n\t\t\tconnection.setDataForSendingReady(false);\n\n\t\t\t// We don't need to know about WRITE events any more, we've written all our data.\n\t\t\t// This is safe from races with new data, due to the session lock in NIO.\n\t\t\tconnection.unsubscribeKey(SelectionKey.OP_WRITE);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/socket/SocketNIODataService.java",
    "content": "package com.network.proxy.vpn.socket;\n\nimport android.util.Log;\n\n\nimport com.network.proxy.vpn.Connection;\nimport com.network.proxy.vpn.TagKt;\n\nimport java.io.IOException;\nimport java.nio.channels.ClosedChannelException;\nimport java.nio.channels.DatagramChannel;\nimport java.nio.channels.SelectableChannel;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.Selector;\nimport java.nio.channels.SocketChannel;\nimport java.nio.channels.spi.AbstractSelectableChannel;\nimport java.util.Iterator;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\n\n/**\n * A service that single-threadedly processes the events around our session connections,\n * entirely via non-blocking NIO.\n * <p>\n * It uses a Selector that fires on outgoing socket events (connected, readable, writable),\n * handles the resulting operations, and keeps those subscriptions up to date.\n */\npublic class SocketNIODataService implements Runnable {\n\n\tprivate final String TAG = TagKt.getTAG(this);\n\tprivate final ReentrantLock nioSelectionLock = new ReentrantLock();\n\tprivate final ReentrantLock nioHandlingLock = new ReentrantLock();\n\tprivate final Selector selector = Selector.open();\n\n\tprivate final SocketChannelReader reader;\n\tprivate final SocketChannelWriter writer;\n\n\tprivate volatile boolean shutdown = false;\n\n\t\n\tpublic SocketNIODataService(ClientPacketWriter clientPacketWriter) throws IOException {\n\t\treader = new SocketChannelReader(clientPacketWriter);\n\t\twriter = new SocketChannelWriter(clientPacketWriter);\n\t}\n\n\t@Override\n\tpublic void run() {\n\t\tLog.d(TAG,\"SocketNIODataService starting in background...\");\n\t\trunTask();\n\t}\n\n\tpublic void registerSession(Connection connection) throws ClosedChannelException {\n\t\tAbstractSelectableChannel channel = connection.getChannel();\n\n\t\tboolean isConnected = channel instanceof DatagramChannel\n\t\t\t\t? ((DatagramChannel) channel).isConnected()\n\t\t\t\t: ((SocketChannel) channel).isConnected();\n\n//\t\tLog.i(TAG, \"Registering new session: \" + session);\n\n\t\tLock selectorLock = lockSelector(selector);\n\t\ttry {\n\t\t\tSelectionKey selectionKey = channel.register(selector,\n\t\t\t\t\tisConnected\n\t\t\t\t\t\t\t? SelectionKey.OP_READ\n\t\t\t\t\t\t\t: SelectionKey.OP_CONNECT\n\t\t\t);\n\t\t\tconnection.setSelectionKey(selectionKey);\n\t\t\tselectionKey.attach(connection);\n//\t\t\tLog.d(TAG, \"Registered selector successfully\");\n\t\t} finally {\n\t\t\tselectorLock.unlock();\n\t\t}\n\t}\n\n\tprivate Lock lockSelector(Selector selector) {\n\t\tboolean gotSelectionLock = nioSelectionLock.tryLock();\n\t\tif (gotSelectionLock) return nioSelectionLock;\n\n\t\tnioHandlingLock.lock(); // Ensure the NIO thread can't do anything on wakeup\n\t\tselector.wakeup();\n\n\t\tnioSelectionLock.lock(); // Actually get the lock we want\n\t\tnioHandlingLock.unlock(); // Release the handling lock, which we no longer care about\n\n\t\treturn nioSelectionLock;\n\t}\n\n\t/**\n\t * If the selector is currently select()ing, wake it up (e.g. to register changes to\n\t * interestOps). If it's not (and so it probably will select() very soon anyway) do nothing.\n\t * This is designed to be run after changing readyOps, to ensure the new ops get monitored\n\t * immediately (and fire immediately, if already ready). Without this, that blocks.\n\t */\n\tpublic void refreshSelect(Connection connection) {\n\t\tboolean gotLock = nioSelectionLock.tryLock();\n\n\t\tif (!gotLock) {\n\t\t\tconnection.getSelectionKey().selector().wakeup();\n\t\t} else {\n\t\t\tnioSelectionLock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * Shut down the NIO thread\n\t */\n\tpublic void shutdown(){\n\t\tthis.shutdown = true;\n\t\tselector.wakeup();\n\t}\n\n\tprivate void runTask(){\n\t\tLog.i(TAG, \"NIO selector is running...\");\n\t\t\n\t\twhile(!shutdown){\n\t\t\ttry {\n\t\t\t\tnioSelectionLock.lockInterruptibly();\n\t\t\t\tselector.select();\n\t\t\t} catch (IOException e) {\n\t\t\t\tLog.e(TAG,\"Error in Selector.select(): \" + e.getMessage());\n\t\t\t\ttry {\n\t\t\t\t\tThread.sleep(100);\n\t\t\t\t} catch (InterruptedException ex) {\n\t\t\t\t\tLog.e(TAG, e.toString());\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t} catch (InterruptedException ex) {\n\t\t\t\tLog.i(TAG, \"Select() interrupted\");\n\t\t\t} finally {\n\t\t\t\tif (nioSelectionLock.isHeldByCurrentThread()) {\n\t\t\t\t\tnioSelectionLock.unlock();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (shutdown) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// A lock here makes it possible to reliably grab the selection lock above\n\t\t\tnioHandlingLock.lock();\n\t\t\ttry {\n\t\t\t\tIterator<SelectionKey> iterator = selector.selectedKeys().iterator();\n\n\t\t\t\twhile (iterator.hasNext()) {\n\t\t\t\t\tSelectionKey key = iterator.next();\n\t\t\t\t\tConnection connection = ((Connection) key.attachment());\n\t\t\t\t\tsynchronized (connection) { // Sessions are locked during processing (no VPN data races)\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tprocessSelectionKey(key);\n\t\t\t\t\t\t} catch (IOException e) {\n\t\t\t\t\t\t\tsynchronized (key) {\n\t\t\t\t\t\t\t\tkey.cancel();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\titerator.remove();\n\t\t\t\t\tif (shutdown) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tnioHandlingLock.unlock();\n\t\t\t}\n\t\t}\n\t\tLog.i(TAG, \"NIO selector shutdown\");\n\t}\n\n\tprivate void processSelectionKey(SelectionKey key) throws IOException {\n\t\tif (!key.isValid()) {\n\t\t\tLog.d(TAG,\"Invalid SelectionKey\");\n\t\t\treturn;\n\t\t}\n\n\t\tSelectableChannel channel = key.channel();\n\n\t\tConnection connection = ((Connection) key.attachment());\n\t\tif (connection == null) {\n\t\t\tLog.w(TAG, \"Key fired with no session attached\");\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tif (channel instanceof SocketChannel && !connection.isConnected() && key.isConnectable()) {\n\t\t\tSocketChannel socketChannel = (SocketChannel) channel;\n\n\t\t\tif (socketChannel.isConnectionPending()) {\n\t\t\t\tboolean connected = socketChannel.finishConnect();\n\t\t\t\tconnection.setConnected(connected);\n\t\t\t} else {\n\t\t\t\tthrow new IllegalStateException(\"TCP channels must either be connected or pending connection\");\n\t\t\t}\n\t\t}\n\n\t\tif (isConnected(channel)) {\n\t\t\tprocessConnectedSelection(key, connection);\n\t\t}\n\t}\n\n\tprivate boolean isConnected(SelectableChannel channel) {\n\t\tif (channel instanceof DatagramChannel) {\n\t\t\treturn ((DatagramChannel) channel).isConnected();\n\t\t} else if (channel instanceof SocketChannel) {\n\t\t\treturn ((SocketChannel) channel).isConnected();\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"isConnected on unexpected channel type: \" + channel);\n\t\t}\n\t}\n\n\tprivate void processConnectedSelection(SelectionKey key, Connection connection) {\n\t\t// Whilst connected, we always want READ and not CONNECT events\n\t\tconnection.unsubscribeKey(SelectionKey.OP_CONNECT);\n\t\tconnection.subscribeKey(SelectionKey.OP_READ);\n\t\tprocessSelectorRead(key, connection);\n\t\tprocessPendingWrite(key, connection);\n\t}\n\n\tprivate void processSelectorRead(SelectionKey selectionKey, Connection connection) {\n\t\tboolean canRead;\n\t\tsynchronized (selectionKey) {\n\t\t\t// There's a race here that requires a lock, as isReadable requires isValid\n\t\t\tcanRead = selectionKey.isValid() && selectionKey.isReadable();\n\t\t}\n\n\t\tif (canRead) reader.read(connection);\n\t}\n\n\tprivate void processPendingWrite(SelectionKey selectionKey, Connection connection) {\n\t\t// Nothing to write? Skip this entirely, and make sure we're not subscribed\n\t\tif (!connection.hasDataToSend() || !connection.isDataForSendingReady()) {\n\t\t\tconnection.unsubscribeKey(SelectionKey.OP_WRITE);\n\t\t\treturn;\n\t\t}\n\n\t\tboolean canWrite;\n\t\tsynchronized (selectionKey) {\n\t\t\t// There's a race here that requires a lock, as isReadable requires isValid\n\t\t\tcanWrite = selectionKey.isValid() && selectionKey.isWritable();\n\t\t}\n\n\t\tif (canWrite) {\n\t\t\tconnection.unsubscribeKey(SelectionKey.OP_WRITE);\n\t\t\twriter.write(connection); // This will resubscribe to OP_WRITE if it can't complete\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/transport/Packet.kt",
    "content": "package com.network.proxy.vpn.transport\n\nimport com.network.proxy.vpn.transport.protocol.IP4Header\nimport com.network.proxy.vpn.transport.protocol.TransportHeader\n\nclass Packet(var ipHeader: IP4Header, var transportHeader: TransportHeader, var buffer: ByteArray) {\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/transport/icmp/ICMPPacket.java",
    "content": "package com.network.proxy.vpn.transport.icmp;\n\n\nimport androidx.annotation.NonNull;\n\npublic class ICMPPacket {\n    // Two ICMP packets we can handle: simple ping & pong\n    public static final byte ECHO_REQUEST_TYPE = 8;\n    public static final byte ECHO_SUCCESS_TYPE = 0;\n\n    // One very common packet we ignore: connection rejection. Unclear why this happens,\n    // random incoming connections that the phone tries to reply to? Nothing we can do though,\n    // as we can't forward ICMP onwards, and we can't usefully respond or react.\n    public static final byte DESTINATION_UNREACHABLE_TYPE = 3;\n\n    public final byte type;\n    final byte code; // 0 for request, 0 for success, 0 - 15 for error subtypes\n\n    final int checksum;\n    final int identifier;\n    final int sequenceNumber;\n\n    final byte[] data;\n\n    ICMPPacket(\n            int type,\n            int code,\n            int checksum,\n            int identifier,\n            int sequenceNumber,\n            byte[] data\n    ) {\n        this.type = (byte) type;\n        this.code = (byte) code;\n        this.checksum = checksum;\n        this.identifier = identifier;\n        this.sequenceNumber = sequenceNumber;\n        this.data = data;\n    }\n\n    @NonNull\n    public String toString() {\n        return \"ICMP packet type \" + type + \"/\" + code + \" id:\" + identifier +\n                \" seq:\" + sequenceNumber + \" and \" + data.length + \" bytes of data\";\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/transport/icmp/ICMPPacketFactory.java",
    "content": "package com.network.proxy.vpn.transport.icmp;\n\n\nimport androidx.annotation.NonNull;\n\nimport com.network.proxy.vpn.transport.protocol.IP4Header;\nimport com.network.proxy.vpn.util.PacketUtil;\n\nimport java.io.ByteArrayOutputStream;\nimport java.nio.ByteBuffer;\n\n\npublic class ICMPPacketFactory {\n\n    public static ICMPPacket parseICMPPacket(@NonNull ByteBuffer stream) {\n        final byte type = stream.get();\n        final byte code = stream.get();\n        final int checksum = stream.getShort();\n\n        final int identifier = stream.getShort();\n        final int sequenceNumber = stream.getShort();\n\n        final byte[] data = new byte[stream.remaining()];\n        stream.get(data);\n\n        return new ICMPPacket(type, code, checksum, identifier, sequenceNumber, data);\n    }\n\n    public static ICMPPacket buildSuccessPacket(ICMPPacket requestPacket) {\n        return new ICMPPacket(\n                0,\n                0,\n                0,\n                requestPacket.identifier,\n                requestPacket.sequenceNumber,\n                requestPacket.data\n        );\n    }\n\n    public static byte[] packetToBuffer(IP4Header ipHeader, ICMPPacket packet) {\n        byte[] ipData = ipHeader.toBytes();\n\n        ByteArrayOutputStream icmpDataBuffer = new ByteArrayOutputStream();\n        icmpDataBuffer.write(packet.type);\n        icmpDataBuffer.write(packet.code);\n\n        icmpDataBuffer.write(asShortBytes(0 /* checksum placeholder */), 0, 2);\n\n        if (packet.type == ICMPPacket.ECHO_REQUEST_TYPE || packet.type == ICMPPacket.ECHO_SUCCESS_TYPE) {\n            icmpDataBuffer.write(asShortBytes(packet.identifier), 0, 2);\n            icmpDataBuffer.write(asShortBytes(packet.sequenceNumber), 0, 2);\n\n            byte[] extraData = packet.data;\n            icmpDataBuffer.write(extraData, 0, extraData.length);\n        } else {\n            throw new IllegalArgumentException(\"Can't serialize unrecognized ICMP packet type\");\n        }\n\n        byte[] icmpPacketData = icmpDataBuffer.toByteArray();\n        byte[] checksum = PacketUtil.INSTANCE.calculateChecksum(icmpPacketData, 0, icmpPacketData.length);\n\n        ByteBuffer resultBuffer = ByteBuffer.allocate(ipData.length + icmpPacketData.length);\n        resultBuffer.put(ipData);\n        resultBuffer.put(icmpPacketData);\n\n        // Replace the checksum placeholder\n        resultBuffer.position(ipData.length + 2);\n        resultBuffer.put(checksum);\n        resultBuffer.position(0);\n\n        byte[] result = new byte[resultBuffer.remaining()];\n        resultBuffer.get(result);\n        return result;\n    }\n\n    private static byte[] asShortBytes(int value) {\n        return ByteBuffer.allocate(2).putShort((short) value).array();\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/IP4Header.kt",
    "content": "package com.network.proxy.vpn.transport.protocol\n\nimport android.util.Log\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\n\n/**\n * IPv4报头的数据结构。\n */\ndata class IP4Header(\n    var ipVersion: Byte = 0, //对于IPv4，其值为4（因此命名为IPv4）。 4bit\n    private var internetHeaderLength: Byte = 0, //头部长度 4bit\n    private var diffTypeOfService: Byte, //差分服务代码点 =>6位\n    private var ecn: Byte = 0, //显式拥塞通知（ECN）\n    var totalLength: Int = 0, //此IP数据包的总长度 16bit\n    var identification: Int = 0, //主要用于唯一标识单个IP数据报的片段组。 16bit\n    private var mayFragment: Boolean, // 1bit   用于指示数据报是否可以分段。\n    private var lastFragment: Boolean, // 1bit   用于指示数据报是否是片段中的最后一个。\n    var fragmentOffset: Short = 0, //13bit，指定特定片段相对于原始未分段的IP数据报的开始的偏移量。\n    private var timeToLive: Byte = 0, //用于防止数据报持续存在。8bit\n    var protocol: Byte = 0, //定义IP数据报的数据部分中使用的协议。 8bit\n    var headerChecksum: Int = 0, //用于对头部进行错误检查的16位字段。 16bit\n    var sourceIP: Int = 0, //发送者的IPv4地址。 32bit\n    var destinationIP: Int = 0 //接收者的IPv4地址。 32bit\n) {\n    //用于控制或识别片段的3比特字段。\n    //bit 0: 保留；必须为零\n    //bit 1: Don't Fragment (DF)\n    //bit 2: More Fragments (MF)\n    private var flag: Byte = initFlag()\n\n    private fun initFlag(): Byte {\n        var initFlag = 0\n        if (mayFragment) {\n            initFlag = 0x40\n        }\n\n        if (lastFragment) {\n            initFlag = (initFlag or 0x20)\n        }\n        return initFlag.toByte()\n    }\n\n    fun setMayFragment(mayFragment: Boolean) {\n        this.mayFragment = mayFragment\n        flag = if (mayFragment) {\n            (flag.toInt() or 0x40).toByte()\n        } else {\n            (flag.toInt() and 0xBF).toByte()\n        }\n    }\n\n    fun getIPHeaderLength(): Int {\n        return internetHeaderLength * 4\n    }\n\n    fun copy(): IP4Header {\n        return IP4Header(\n            ipVersion, internetHeaderLength, diffTypeOfService, ecn, totalLength, identification,\n            mayFragment, lastFragment, fragmentOffset, timeToLive, protocol, headerChecksum,\n            sourceIP, destinationIP\n        )\n    }\n\n    fun toBytes(): ByteArray {\n        val buffer = ByteBuffer.allocate(getIPHeaderLength())\n        buffer.order(ByteOrder.BIG_ENDIAN)\n        val versionAndHeaderLength = (ipVersion.toInt() shl 4) + internetHeaderLength\n        buffer.put(versionAndHeaderLength.toByte())\n\n        val typeOfService: Byte = (diffTypeOfService.toInt() shl 2 and (ecn\n            .toInt() and 0xFF)).toByte()\n        buffer.put(typeOfService)\n\n        buffer.putShort(totalLength.toShort())\n        buffer.putShort(identification.toShort())\n\n        //组合标志和部分片段偏移\n        buffer.put((fragmentOffset.toInt() shr 8 and 0x1F or flag.toInt()).toByte())\n        buffer.put(fragmentOffset.toByte())\n\n        buffer.put(timeToLive)\n        buffer.put(protocol)\n        buffer.putShort(headerChecksum.toShort())\n        buffer.putInt(sourceIP)\n        buffer.putInt(destinationIP)\n        return buffer.array()\n    }\n}\n\nobject IPPacketFactory {\n    private const val IP4_HEADER_SIZE = 20\n    private const val IP4_VERSION = 0x04\n\n    /**\n     * 从给定的ByteBuffer流创建IPv4标头\n     */\n    fun createIP4Header(buffer: ByteBuffer): IP4Header? {\n        if (buffer.remaining() < IP4_HEADER_SIZE) {\n            throw IllegalArgumentException(\"IP header byte array must have at least $IP4_HEADER_SIZE bytes\")\n        }\n\n        val versionAndHeaderLength: Byte = buffer.get()\n        val ipVersion = (versionAndHeaderLength.toInt() shr 4).toByte()\n        if (ipVersion.toInt() != IP4_VERSION) {\n            Log.e(\"IPPacketFactory\", \"Invalid IP version $ipVersion\")\n            return null\n        }\n\n        val internetHeaderLength = (versionAndHeaderLength.toInt() and 0x0F).toByte()\n\n        val typeOfService = buffer.get().toInt()\n        val diffTypeOfService: Byte = (typeOfService shr 2).toByte();\n        val ecn: Byte = (typeOfService and 0x03).toByte()\n\n        val totalLength: Int = buffer.getShort().toInt()\n        val identification: Int = buffer.getShort().toInt()\n\n        val flagsAndFragmentOffset: Short = buffer.getShort()\n        val mayFragment = flagsAndFragmentOffset.toInt() and 0x4000 != 0\n        val lastFragment = flagsAndFragmentOffset.toInt() and 0x2000 != 0\n        val fragmentOffset = (flagsAndFragmentOffset.toInt() and 0x1FFF).toShort()\n\n        val timeToLive: Byte = buffer.get()\n        val protocol: Byte = buffer.get()\n        val checksum: Int = buffer.getShort().toInt()\n        val sourceIp: Int = buffer.getInt()\n        val desIp: Int = buffer.getInt()\n\n        if (internetHeaderLength > 5) {\n            // drop the IP option\n            for (i in 0 until (internetHeaderLength - 5)) {\n                buffer.getInt()\n            }\n        }\n\n        return IP4Header(\n            ipVersion, internetHeaderLength, diffTypeOfService, ecn, totalLength, identification,\n            mayFragment, lastFragment, fragmentOffset, timeToLive, protocol, checksum,\n            sourceIp, desIp\n        )\n    }\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TCPHeader.kt",
    "content": "package com.network.proxy.vpn.transport.protocol\n\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\n\n/**\n * TCP报头的数据结构。\n */\nclass TCPHeader(\n    private var sourcePort: Int = 0, //源端口号 16bit\n    private var destinationPort: Int = 0, //目的端口号 16bit\n    var sequenceNumber: Long = 0, //序列号 32bit\n    var ackNumber: Long = 0, //确认号 32bit\n    var dataOffset: Int = 0, //数据偏移4bit\n    var isNS: Boolean = false, //ECN-nonce concealment protection (experimental: see RFC 3540)\n    var flags: Int = 0, //标志位 9bit\n    var windowSize: Int = 0, //窗口大小 16bit\n    var checksum: Int = 0, //校验和 16bit\n    private var urgentPointer: Int = 0, //紧急指针 16bit\n    var options: ByteArray? = null //选项\n) : TransportHeader {\n\n    //options\n    var maxSegmentSize: Short = 0\n    private var windowScale: Byte = 0\n    private var isSelectiveAckPermitted = false\n    var timeStampSender = 0\n    var timeStampReplyTo = 0\n\n    companion object {\n        private const val END_OF_OPTIONS_LIST: Byte = 0\n        private const val NO_OPERATION: Byte = 1\n        private const val MAX_SEGMENT_SIZE: Byte = 2\n        private const val WINDOW_SCALE: Byte = 3\n        private const val SELECTIVE_ACK_PERMITTED: Byte = 4\n        private const val TIME_STAMP: Byte = 8\n    }\n\n    fun isSYN(): Boolean {\n        return flags and 0x02 != 0\n    }\n\n    fun isFIN(): Boolean {\n        return flags and 0x01 != 0\n    }\n\n    fun isRST(): Boolean {\n        return flags and 0x04 != 0\n    }\n\n    fun isPSH(): Boolean {\n        return flags and 0x08 != 0\n    }\n\n    fun isACK(): Boolean {\n        return flags and 0x10 != 0\n    }\n\n    fun isURG(): Boolean {\n        return flags and 0x20 != 0\n    }\n\n    fun isECE(): Boolean {\n        return flags and 0x40 != 0\n    }\n\n    fun isCWR(): Boolean {\n        return flags and 0x80 != 0\n    }\n\n    fun setIsRST(isRST: Boolean) {\n        flags = if (isRST) {\n            (flags or 0x04)\n        } else {\n            (flags and 0xFB)\n        }\n    }\n\n    fun setIsSYN(isSYN: Boolean) {\n        flags = if (isSYN) {\n            (flags or 0x02)\n        } else {\n            (flags and 0xFD)\n        }\n    }\n\n    fun setIsFIN(isFIN: Boolean) {\n        flags = if (isFIN) {\n            (flags or 0x01)\n        } else {\n            (flags and 0xFE)\n        }\n    }\n\n    fun setIsPSH(isPSH: Boolean) {\n        flags = if (isPSH) {\n            (flags or 0x08)\n        } else {\n            (flags and 0xF7)\n        }\n    }\n\n    fun setIsACK(isACK: Boolean) {\n        flags = if (isACK) {\n            (flags or 0x10)\n        } else {\n            (flags and 0xEF)\n        }\n    }\n\n    fun getTCPHeaderLength(): Int {\n        return dataOffset * 4\n    }\n\n    fun toBytes(): ByteArray {\n        val tcpHeaderLength = getTCPHeaderLength()\n        val tcpHeader = ByteArray(tcpHeaderLength)\n        val byteBuffer = ByteBuffer.wrap(tcpHeader)\n        byteBuffer.order(ByteOrder.BIG_ENDIAN)\n\n        byteBuffer.putShort(sourcePort.toShort())\n        byteBuffer.putShort(destinationPort.toShort())\n\n        byteBuffer.putInt(sequenceNumber.toInt())\n        byteBuffer.putInt(ackNumber.toInt())\n\n        //is ns and data offset\n        byteBuffer.put(((dataOffset shl 4) and 0xF0 or (if (isNS) 0x1 else 0x0)).toByte())\n        byteBuffer.put(flags.toByte())\n        byteBuffer.putShort(windowSize.toShort())\n        byteBuffer.putShort(checksum.toShort())\n        byteBuffer.putShort(urgentPointer.toShort())\n//        encodeTcpOptions()?.let {\n//            byteBuffer.put(it)\n//        }\n\n        return tcpHeader\n    }\n\n    fun copy(): TCPHeader {\n        return TCPHeader(\n            sourcePort, destinationPort, sequenceNumber, ackNumber,\n            dataOffset, isNS, flags, windowSize, checksum, urgentPointer,\n            options\n        )\n    }\n\n    private fun handleTcpOptions() {\n        if (options == null) {\n            return\n        }\n\n        var index = 0\n        val packet = ByteBuffer.wrap(options!!)\n        val optionsSize = options!!.size\n\n        while (index < optionsSize) {\n            val optionKind = packet.get()\n            index++\n            if (optionKind == END_OF_OPTIONS_LIST || optionKind == NO_OPERATION) {\n                continue\n            }\n            val size = packet.get()\n            index++\n            when (optionKind) {\n                MAX_SEGMENT_SIZE -> {\n                    maxSegmentSize = packet.getShort()\n                    index += 2\n                }\n\n                WINDOW_SCALE -> {\n                    windowScale = packet.get()\n                    index++\n                }\n\n                SELECTIVE_ACK_PERMITTED -> isSelectiveAckPermitted = true\n                TIME_STAMP -> {\n                    timeStampSender = packet.getInt()\n                    timeStampReplyTo = packet.getInt()\n                    index += 8\n                }\n\n                else -> {\n                    skipRemainingOptions(packet, size.toInt())\n                    index = index + size - 2\n                }\n            }\n        }\n    }\n\n    private fun skipRemainingOptions(packet: ByteBuffer, size: Int) {\n        for (i in 2 until size) {\n            packet.get()\n        }\n    }\n\n    override fun getSourcePort(): Int {\n        return sourcePort\n    }\n\n    override fun getDestinationPort(): Int {\n        return destinationPort\n    }\n\n    fun setSourcePort(sourcePort: Int) {\n        this.sourcePort = sourcePort\n    }\n\n    fun setDestinationPort(destinationPort: Int) {\n        this.destinationPort = destinationPort\n    }\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TCPPacketFactory.kt",
    "content": "package com.network.proxy.vpn.transport.protocol\n\nimport com.network.proxy.vpn.transport.Packet\nimport com.network.proxy.vpn.util.PacketUtil\nimport java.nio.ByteBuffer\nimport java.util.concurrent.ThreadLocalRandom\n\nobject TCPPacketFactory {\n\n    private const val TCP_HEADER_LENGTH = 20\n\n    /**\n     * 从tcp报文创建tcpHeader\n     */\n    @JvmStatic\n    fun createTCPHeader(byteBuffer: ByteBuffer): TCPHeader {\n        if (byteBuffer.remaining() < TCP_HEADER_LENGTH) {\n            throw IllegalArgumentException(\"Invalid TCP Header Length\")\n        }\n\n        val sourcePort: Int = byteBuffer.getShort().toInt() and 0xFFFF\n        val destinationPort: Int = byteBuffer.getShort().toInt() and 0xFFFF\n        val sequenceNumber: Long = byteBuffer.getInt().toLong()\n        val ackNumber: Long = byteBuffer.getInt().toLong()\n\n        val dataOffsetAndReserved = byteBuffer.get()\n        val dataOffset = (dataOffsetAndReserved.toInt() and 0xF0) shr 4\n        val isNs: Boolean = dataOffsetAndReserved.toInt() and 0x1 > 0x0\n\n        val flags = byteBuffer.get().toInt()\n\n        val window = byteBuffer.short.toInt()\n        val checksum = byteBuffer.short.toInt()\n        val urgentPointer = byteBuffer.short.toInt()\n\n        var optionsAndPadding: ByteArray? = null\n        val optionsSize = dataOffset - 5\n        if (optionsSize > 0) {\n            optionsAndPadding = ByteArray(optionsSize * 4)\n            byteBuffer.get(optionsAndPadding, 0, optionsSize * 4)\n        }\n        return TCPHeader(\n            sourcePort, destinationPort, sequenceNumber, ackNumber,\n            dataOffset, isNs, flags, window, checksum, urgentPointer, optionsAndPadding\n        )\n    }\n\n    /**\n     * 创建带有RST标志的数据包，以便在需要重置时发送到客户端。\n     */\n    fun createRstData(ipHeader: IP4Header, tcpHeader: TCPHeader, dataLength: Int): ByteArray {\n        val ip = ipHeader.copy()\n        val tcp = tcpHeader.copy()\n\n        var ackNumber: Long = 0\n        var seqNumber: Long = 0\n\n        if (tcp.ackNumber > 0) {\n            seqNumber = tcp.ackNumber\n        } else {\n            ackNumber = tcp.sequenceNumber + dataLength\n        }\n\n        tcp.ackNumber = ackNumber\n        tcp.sequenceNumber = seqNumber\n\n        //将IP从源翻转到目标\n        flipIp(ip, tcp)\n\n        ip.identification = 0\n\n        tcp.flags = 0\n        tcp.isNS = false\n        tcp.setIsRST(true)\n\n        tcp.dataOffset = 5\n        tcp.options = null\n        tcp.windowSize = 0\n\n        //重新计算IP长度\n        val totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()\n\n        ip.totalLength = totalLength\n\n        return createPacketData(ip, tcp, null)\n    }\n\n    /**\n     * 创建数据包数据以发送回客户端\n     */\n    @JvmStatic\n    fun createResponsePacketData(\n        ipHeader: IP4Header, tcpHeader: TCPHeader, packetData: ByteArray?, isPsh: Boolean,\n        ackNumber: Long, seqNumber: Long, timeSender: Int, timeReplyTo: Int\n    ): ByteArray {\n        val ip = ipHeader.copy()\n        val tcp = tcpHeader.copy()\n\n        flipIp(ip, tcp)\n        tcp.ackNumber = ackNumber\n        tcp.sequenceNumber = seqNumber\n        ip.identification = PacketUtil.getPacketId()\n\n        //总是发送ACK\n\n        //ACK is always sent\n        tcp.setIsACK(true)\n        tcp.setIsSYN(false)\n        tcp.setIsPSH(isPsh)\n        tcp.setIsFIN(false)\n        tcp.timeStampSender = timeSender\n        tcp.timeStampReplyTo = timeReplyTo\n        tcp.dataOffset = 5\n        tcp.options = null\n\n        var totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()\n        if (packetData != null) {\n            totalLength += packetData.size\n        }\n        ip.totalLength = totalLength\n\n        return createPacketData(ip, tcp, packetData)\n    }\n\n\n    /**\n     * 向客户端确认服务器已收到请求。\n     */\n    @JvmStatic\n    fun createResponseAckData(\n        ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: Long\n    ): ByteArray {\n        val ip = ipHeader.copy()\n        val tcp = tcpHeader.copy()\n\n        flipIp(ip, tcp)\n        val seqNumber = tcp.ackNumber\n        tcp.ackNumber = ackToClient\n        tcp.sequenceNumber = seqNumber\n\n        ip.identification = PacketUtil.getPacketId()\n\n        //ACK\n        tcp.setIsACK(true)\n        tcp.setIsSYN(false)\n        tcp.setIsPSH(false)\n        tcp.setIsFIN(false)\n        tcp.dataOffset = 5\n        tcp.options = null\n\n        ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()\n        return createPacketData(ip, tcp, null)\n    }\n\n    //将IP从源翻转到目标\n    private fun flipIp(ip: IP4Header, tcp: TCPHeader) {\n        val sourceIp = ip.destinationIP\n        val destIp = ip.sourceIP\n        val sourcePort = tcp.getDestinationPort()\n        val destPort = tcp.getSourcePort()\n\n        ip.destinationIP = destIp\n        ip.sourceIP = sourceIp\n        tcp.setDestinationPort(destPort)\n        tcp.setSourcePort(sourcePort)\n    }\n\n    /**\n     * 通过写回客户端流创建SYN-ACK数据包数据\n     */\n    fun createSynAckPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader): Packet {\n        val ip = ipHeader.copy()\n        val tcp = tcpHeader.copy()\n\n        flipIp(ip, tcp)\n\n        //ack = received sequence + 1\n        val ackNumber = tcpHeader.sequenceNumber + 1\n        tcp.ackNumber = ackNumber\n\n        //服务器生成的初始序列号\n        val seqNumber = ThreadLocalRandom.current().nextLong(0, 100000)\n        tcp.sequenceNumber = seqNumber\n\n        //SYN-ACK\n        tcp.setIsACK(true)\n        tcp.setIsSYN(true)\n\n        tcp.timeStampReplyTo = tcp.timeStampSender\n        tcp.timeStampSender = PacketUtil.currentTime\n\n        tcp.dataOffset = 5\n        tcp.options = null\n        ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()\n\n        return Packet(ip, tcp, createPacketData(ip, tcp, null))\n    }\n\n    /**\n     * 创建发送到客户端的FIN-ACK\n     */\n    fun createFinAckData(\n        ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: Long,\n        seqToClient: Long, isFin: Boolean, isAck: Boolean\n    ): ByteArray {\n        val ip = ipHeader.copy()\n        val tcp = tcpHeader.copy()\n\n        flipIp(ip, tcp)\n\n        tcp.ackNumber = ackToClient\n        tcp.sequenceNumber = seqToClient\n        ip.identification = PacketUtil.getPacketId()\n\n        //ACK\n        tcp.setIsACK(isAck)\n        tcp.setIsSYN(false)\n        tcp.setIsPSH(false)\n        tcp.setIsFIN(isFin)\n\n        tcp.dataOffset = 5\n        tcp.options = null\n\n        ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()\n        return createPacketData(ip, tcp, null)\n    }\n\n    fun createFinData(\n        ip: IP4Header, tcp: TCPHeader, ackNumber: Long, seqNumber: Long,\n        timeSender: Int, timeReplyTo: Int\n    ): ByteArray {\n        //将IP从源翻转到目标\n        flipIp(ip, tcp)\n\n        tcp.ackNumber = ackNumber\n        tcp.sequenceNumber = seqNumber\n\n        ip.identification = PacketUtil.getPacketId()\n\n        tcp.timeStampReplyTo = timeReplyTo\n        tcp.timeStampSender = timeSender\n\n        tcp.flags = 0\n        tcp.isNS = false\n        tcp.setIsACK(true)\n        tcp.setIsFIN(true)\n\n        tcp.dataOffset = 5\n        tcp.options = null\n        //窗口大小应为零\n        tcp.windowSize = 0\n\n        ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()\n        return createPacketData(ip, tcp, null)\n    }\n\n    /**\n     * 从tcpHeader创建tcp报文\n     */\n    private fun createPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader, data: ByteArray?):\n            ByteArray {\n        val dataLength = data?.size ?: 0\n\n        val buffer =\n            ByteBuffer.allocate(ipHeader.getIPHeaderLength() + tcpHeader.getTCPHeaderLength() + dataLength)\n        val ipBuffer = ipHeader.toBytes()\n        val tcpBuffer = tcpHeader.toBytes()\n\n        buffer.put(ipBuffer)\n        buffer.put(tcpBuffer)\n\n        data?.let { buffer.put(it) }\n\n        val zero = byteArrayOf(0, 0)\n        //计算前先将校验和清零\n        buffer.position(10)\n        buffer.put(zero)\n\n        val ipChecksum = PacketUtil.calculateChecksum(buffer.array(), 0, ipBuffer.size)\n        buffer.position(10)\n        buffer.put(ipChecksum)\n\n        val tcpStart = ipBuffer.size\n        buffer.position(tcpStart + 16)\n        buffer.put(zero)\n\n        val tcpChecksum = PacketUtil.calculateTCPHeaderChecksum(\n            buffer.array(), tcpStart, tcpBuffer.size + dataLength,\n            ipHeader.destinationIP, ipHeader.sourceIP\n        )\n\n        //将新的校验和写回阵列\n        buffer.position(tcpStart + 16)\n        buffer.put(tcpChecksum)\n        return buffer.array()\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TransportHeader.kt",
    "content": "package com.network.proxy.vpn.transport.protocol\n\ninterface TransportHeader {\n    fun getSourcePort(): Int\n    fun getDestinationPort(): Int\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/UDPHeader.kt",
    "content": "package com.network.proxy.vpn.transport.protocol\n\n\nimport com.network.proxy.vpn.util.PacketUtil\nimport java.nio.ByteBuffer\n\n\n/**\n * UDP报头的数据结构。\n */\ndata class UDPHeader(\n    var sourcePort: Int = 0, //源端口号 16bit\n    var destinationPort: Int = 0, //目的端口号 16bit\n    var length: Int = 0, //UDP数据报长度 16bit\n    var checksum: Int = 0 //校验和 16bit\n)\n\n\nobject UDPPacketFactory {\n    @JvmStatic\n    fun createUDPHeader(stream: ByteBuffer): UDPHeader {\n        require(stream.remaining() >= 8) { \"Minimum UDP header is 8 bytes.\" }\n        val srcPort = stream.getShort().toInt() and 0xffff\n        val destPort = stream.getShort().toInt() and 0xffff\n        val length = stream.getShort().toInt() and 0xffff\n        val checksum = stream.getShort().toInt()\n        return UDPHeader(srcPort, destPort, length, checksum)\n    }\n\n    /**\n     * 创建用于响应vpn客户端的数据包\n     */\n    @JvmStatic\n    fun createResponsePacket(ip: IP4Header, udp: UDPHeader, packetData: ByteArray?): ByteArray {\n        val buffer: ByteArray\n        var udpLen = 8\n        if (packetData != null) {\n            udpLen += packetData.size\n        }\n        val srcPort = udp.destinationPort\n        val destPort = udp.sourcePort\n        val ipHeader = ip.copy()\n        val srcIp = ip.destinationIP\n        val destIp = ip.sourceIP\n        ipHeader.setMayFragment(false)\n        ipHeader.sourceIP = srcIp\n        ipHeader.destinationIP = destIp\n        ipHeader.identification = PacketUtil.getPacketId()\n\n        //ip的长度是整个数据包的长度 => IP header length + UDP header length (8) + UDP body length\n        val totalLength = ipHeader.getIPHeaderLength() + udpLen\n        ipHeader.totalLength = totalLength\n        buffer = ByteArray(totalLength)\n        val ipData = ipHeader.toBytes()\n\n        // clear IP checksum\n        ipData[11] = 0\n        ipData[10] = 0\n\n        //calculate checksum for IP header\n        val ipChecksum = PacketUtil.calculateChecksum(ipData, 0, ipData.size)\n        //write result of checksum back to buffer\n        System.arraycopy(ipChecksum, 0, ipData, 10, 2)\n        System.arraycopy(ipData, 0, buffer, 0, ipData.size)\n\n        //copy UDP header to buffer\n        var start = ipData.size\n        val intContainer = ByteArray(4)\n        PacketUtil.writeIntToBytes(srcPort, intContainer, 0)\n\n        //extract the last two bytes of int value\n        System.arraycopy(intContainer, 2, buffer, start, 2)\n        start += 2\n\n        PacketUtil.writeIntToBytes(destPort, intContainer, 0)\n        System.arraycopy(intContainer, 2, buffer, start, 2)\n        start += 2\n        PacketUtil.writeIntToBytes(udpLen, intContainer, 0)\n        System.arraycopy(intContainer, 2, buffer, start, 2)\n        start += 2\n\n        val checksum: Short = 0\n        PacketUtil.writeIntToBytes(checksum.toInt(), intContainer, 0)\n        System.arraycopy(intContainer, 2, buffer, start, 2)\n        start += 2\n\n        //now copy udp data\n        if (packetData != null) System.arraycopy(packetData, 0, buffer, start, packetData.size)\n        return buffer\n    }\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/util/PacketUtil.kt",
    "content": "package com.network.proxy.vpn.util\n\nimport android.util.Log\nimport com.network.proxy.vpn.formatTag\nimport com.network.proxy.vpn.transport.protocol.IP4Header\nimport com.network.proxy.vpn.transport.protocol.TCPHeader\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\n\n/**\n * Helper class to perform various useful task\n */\nobject PacketUtil {\n    @get:Synchronized\n    private var packetId = 0\n    fun getPacketId() = packetId++\n\n    val currentTime: Int\n        get() = (System.currentTimeMillis() / 1000).toInt()\n\n    /**\n     * convert int to byte array\n     * [...](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html)\n     *\n     * @param value  int value 32 bits\n     * @param buffer array of byte to write to\n     * @param offset position to write to\n     */\n    fun writeIntToBytes(value: Int, buffer: ByteArray, offset: Int) {\n        if (buffer.size - offset < 4) {\n            return\n        }\n        buffer[offset] = (value ushr 24 and 0x000000FF).toByte()\n        buffer[offset + 1] = (value shr 16 and 0x000000FF).toByte()\n        buffer[offset + 2] = (value shr 8 and 0x000000FF).toByte()\n        buffer[offset + 3] = (value and 0x000000FF).toByte()\n    }\n\n    /**\n     * convert array of max 4 bytes to int\n     *\n     * @param buffer byte array\n     * @param start  Starting point to be read in byte array\n     * @param length Length to be read\n     * @return value of int\n     */\n    fun getNetworkInt(buffer: ByteArray, start: Int, length: Int): Int {\n        var value = 0\n        var end = start + Math.min(length, 4)\n        if (end > buffer.size) end = buffer.size\n        for (i in start until end) {\n            value = value or (buffer[i].toInt() and 0xFF)\n            if (i < end - 1) value = value shl 8\n        }\n        return value\n    }\n\n    /**\n     * validate TCP header checksum\n     *\n     * @param source      Source Port\n     * @param destination Destination Port\n     * @param data        Payload\n     * @param tcpLength   TCP Header length\n     * @return boolean\n     */\n    fun isValidTCPChecksum(\n        source: Int, destination: Int,\n        data: ByteArray, tcpLength: Short, tcpOffset: Int\n    ): Boolean {\n        var buffersize = tcpLength + 12\n        var isodd = false\n        if (buffersize % 2 != 0) {\n            buffersize++\n            isodd = true\n        }\n        val buffer = ByteBuffer.allocate(buffersize)\n        buffer.putInt(source)\n        buffer.putInt(destination)\n        buffer.put(0.toByte()) //reserved => 0\n        buffer.put(6.toByte()) //TCP protocol => 6\n        buffer.putShort(tcpLength)\n        buffer.put(data, tcpOffset, tcpLength.toInt())\n        if (isodd) {\n            buffer.put(0.toByte())\n        }\n        return isValidIPChecksum(buffer.array(), buffersize)\n    }\n\n    /**\n     * validate IP Header checksum\n     *\n     * @param data byte stream\n     * @return boolean\n     */\n    private fun isValidIPChecksum(data: ByteArray, length: Int): Boolean {\n        var start = 0\n        var sum = 0\n        while (start < length) {\n            sum += getNetworkInt(data, start, 2)\n            start = start + 2\n        }\n\n        //carry over one's complement\n        while (sum shr 16 > 0) sum = (sum and 0xffff) + (sum shr 16)\n\n        //flip the bit to get one' complement\n        sum = sum.inv()\n        val buffer = ByteBuffer.allocate(4)\n        buffer.putInt(sum)\n        return buffer.getShort(2).toInt() == 0\n    }\n\n    fun calculateChecksum(data: ByteArray, offset: Int, length: Int): ByteArray {\n        var start = offset\n        var sum = 0\n        while (start < length) {\n            sum += getNetworkInt(data, start, 2)\n            start = start + 2\n        }\n        //carry over one's complement\n        while (sum shr 16 > 0) {\n            sum = (sum and 0xffff) + (sum shr 16)\n        }\n        //flip the bit to get one' complement\n        sum = sum.inv()\n\n        //extract the last two byte of int\n        val checksum = ByteArray(2)\n        checksum[0] = (sum shr 8).toByte()\n        checksum[1] = sum.toByte()\n        return checksum\n    }\n\n    fun calculateTCPHeaderChecksum(\n        data: ByteArray,\n        offset: Int,\n        tcplength: Int,\n        destip: Int,\n        sourceip: Int\n    ): ByteArray {\n        var buffersize = tcplength + 12\n        var odd = false\n        if (buffersize % 2 != 0) {\n            buffersize++\n            odd = true\n        }\n        val buffer = ByteBuffer.allocate(buffersize)\n        buffer.order(ByteOrder.BIG_ENDIAN)\n\n        //create virtual header\n        buffer.putInt(sourceip)\n        buffer.putInt(destip)\n        buffer.put(0.toByte()) //reserved => 0\n        buffer.put(6.toByte()) //tcp protocol => 6\n        buffer.putShort(tcplength.toShort())\n\n        //add actual header + data\n        buffer.put(data, offset, tcplength)\n\n        //padding last byte to zero\n        if (odd) {\n            buffer.put(0.toByte())\n        }\n        val tcparray = buffer.array()\n        return calculateChecksum(tcparray, 0, buffersize)\n    }\n\n    fun intToIPAddress(addressInt: Int): String {\n        return (addressInt ushr 24 and 0x000000FF).toString() + \".\" +\n                (addressInt ushr 16 and 0x000000FF) + \".\" +\n                (addressInt ushr 8 and 0x000000FF) + \".\" +\n                (addressInt and 0x000000FF)\n    }\n\n    fun getOutput(\n        ipHeader: IP4Header, tcpheader: TCPHeader,\n        packetData: ByteArray\n    ): String {\n        val tcpLength = (packetData.size -\n                ipHeader.getIPHeaderLength()).toShort()\n        val isValidChecksum = isValidTCPChecksum(\n            ipHeader.sourceIP, ipHeader.destinationIP,\n            packetData, tcpLength, ipHeader.getIPHeaderLength()\n        )\n        val isValidIPChecksum = isValidIPChecksum(\n            packetData,\n            ipHeader.getIPHeaderLength()\n        )\n        val packetBodyLength = (packetData.size - ipHeader.getIPHeaderLength()\n                - tcpheader.getTCPHeaderLength())\n        val str = StringBuilder(\"\\r\\nIP Version: \")\n            .append(ipHeader.ipVersion.toInt())\n            .append(\"\\r\\nProtocol: \").append(ipHeader.protocol.toInt())\n            .append(\"\\r\\nID# \").append(ipHeader.identification)\n            .append(\"\\r\\nTotal Length: \").append(ipHeader.totalLength)\n            .append(\"\\r\\nData Length: \").append(packetBodyLength)\n            .append(\"\\r\\nDest: \").append(intToIPAddress(ipHeader.destinationIP))\n            .append(\":\").append(tcpheader.getDestinationPort())\n            .append(\"\\r\\nSrc: \").append(intToIPAddress(ipHeader.sourceIP))\n            .append(\":\").append(tcpheader.getSourcePort())\n            .append(\"\\r\\nACK: \").append(tcpheader.ackNumber)\n            .append(\"\\r\\nSeq: \").append(tcpheader.sequenceNumber)\n            .append(\"\\r\\nIP Header length: \").append(ipHeader.getIPHeaderLength())\n            .append(\"\\r\\nTCP Header length: \").append(tcpheader.getTCPHeaderLength())\n            .append(\"\\r\\nACK: \").append(tcpheader.isACK())\n            .append(\"\\r\\nSYN: \").append(tcpheader.isSYN())\n            .append(\"\\r\\nCWR: \").append(tcpheader.isCWR())\n            .append(\"\\r\\nECE: \").append(tcpheader.isECE())\n            .append(\"\\r\\nFIN: \").append(tcpheader.isFIN())\n            .append(\"\\r\\nNS: \").append(tcpheader.isNS)\n            .append(\"\\r\\nPSH: \").append(tcpheader.isPSH())\n            .append(\"\\r\\nRST: \").append(tcpheader.isRST())\n            .append(\"\\r\\nURG: \").append(tcpheader.isURG())\n            .append(\"\\r\\nIP checksum: \").append(ipHeader.headerChecksum)\n            .append(\"\\r\\nIs Valid IP Checksum: \").append(isValidIPChecksum)\n            .append(\"\\r\\nTCP Checksum: \").append(tcpheader.checksum)\n            .append(\"\\r\\nIs Valid TCP checksum: \").append(isValidChecksum)\n            .append(\"\\r\\nFragment Offset: \").append(ipHeader.fragmentOffset.toInt())\n            .append(\"\\r\\nWindow: \").append(tcpheader.windowSize)\n            .append(\"\\r\\nData Offset: \").append(tcpheader.dataOffset)\n        return str.toString()\n    }\n\n    /**\n     * detect packet corruption flag in tcp options sent from client ACK\n     *\n     * @param tcpHeader TCPHeader\n     * @return boolean\n     */\n    fun isPacketCorrupted(tcpHeader: TCPHeader): Boolean {\n        val options = tcpHeader.options\n        if (options != null) {\n            var i = 0\n            while (i < options.size) {\n                val kind = options[i]\n                if (kind.toInt() == 0 || kind.toInt() == 1) {\n                } else if (kind.toInt() == 2) {\n                    i += 3\n                } else if (kind.toInt() == 3 || kind.toInt() == 14) {\n                    i += 2\n                } else if (kind.toInt() == 4) {\n                    i++\n                } else if (kind.toInt() == 5 || kind.toInt() == 15) {\n                    i = i + options[++i] - 2\n                } else if (kind.toInt() == 8) {\n                    i += 9\n                } else if (kind.toInt() == 23) {\n                    return true\n                } else {\n                    Log.e(\n                        formatTag(PacketUtil::class.java.name),\n                        \"unknown option: $kind\"\n                    )\n                }\n                i++\n            }\n        }\n        return false\n    }\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/util/ProcessInfoManager.kt",
    "content": "package com.network.proxy.vpn.util\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport android.os.Build\nimport android.os.Process\nimport android.system.OsConstants\nimport android.util.Log\nimport androidx.annotation.RequiresApi\nimport com.network.proxy.ProxyVpnService\nimport com.network.proxy.plugin.ProcessInfo\nimport com.network.proxy.vpn.Connection\nimport kotlinx.coroutines.CoroutineScope\nimport java.io.File\nimport java.net.InetSocketAddress\nimport java.nio.channels.SocketChannel\nimport java.util.concurrent.TimeUnit\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n/**\n * 进程信息管理器，用于获取进程信息\n * @author wanghongen\n */\nclass ProcessInfoManager private constructor() {\n    companion object {\n        @Suppress(\"all\")\n        val instance = ProcessInfoManager()\n    }\n\n    class NetworkInfo(val uid: Int, val remoteHost: String, val remotePort: Int)\n\n    private val localPortCache =\n        SimpleCache<Int, NetworkInfo>(10_000, 60, TimeUnit.SECONDS)\n\n\n    private val appInfoCache = SimpleCache<Int, ProcessInfo>(10_000, 300, TimeUnit.SECONDS)\n\n\n    var activity: Context? = null\n\n    @RequiresApi(Build.VERSION_CODES.N)\n    fun setConnectionOwnerUid(connection: Connection) {\n        CoroutineScope(Dispatchers.IO).launch {\n\n            val sourceAddress =\n                InetSocketAddress(PacketUtil.intToIPAddress(connection.sourceIp), connection.sourcePort)\n            val destinationAddress = InetSocketAddress(\n                PacketUtil.intToIPAddress(connection.destinationIp), connection.destinationPort\n            )\n\n            val uid = getProcessInfoUid(sourceAddress, destinationAddress)\n            val channel = connection.channel\n            if (uid != null && uid != Process.INVALID_UID && channel is SocketChannel && channel.isOpen) {\n                try {\n                    val localAddress = channel.localAddress as InetSocketAddress\n                    val networkInfo =\n                        NetworkInfo(uid, destinationAddress.hostString, destinationAddress.port)\n                    localPortCache.put(localAddress.port, networkInfo)\n                } catch (e: java.nio.channels.ClosedChannelException) {\n                    Log.w(\"ProcessInfoManager\", \"setConnectionOwnerUid\", e)\n                }\n            }\n        }\n    }\n\n    fun removeConnection(connection: Connection) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {\n            return\n        }\n\n        val channel = connection.channel\n        if (channel is SocketChannel && channel.isOpen) {\n            try {\n                val localAddress = channel.localAddress as InetSocketAddress\n                localPortCache.remove(localAddress.port)\n            } catch (e: java.nio.channels.ClosedChannelException) {\n                Log.w(\"ProcessInfoManager\", \"removeConnection\", e)\n            }\n        }\n    }\n\n    @RequiresApi(Build.VERSION_CODES.N)\n    private fun getProcessInfoUid(\n        localAddress: InetSocketAddress, remoteAddress: InetSocketAddress\n    ): Int? {\n//        Log.d(TAG, \"getProcessInfo: $localAddress $remoteAddress\")\n\n        if (activity == null) {\n            return null\n        }\n\n        try {\n            val connectivityManager: ConnectivityManager =\n                activity!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n\n            val uid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                connectivityManager.getConnectionOwnerUid(\n                    OsConstants.IPPROTO_TCP, localAddress, remoteAddress\n                )\n            } else {\n                val method = ConnectivityManager::class.java.getMethod(\n                    \"getConnectionOwnerUid\",\n                    Int::class.javaPrimitiveType,\n                    InetSocketAddress::class.java,\n                    InetSocketAddress::class.java\n                )\n                method.invoke(\n                    connectivityManager, OsConstants.IPPROTO_TCP, localAddress, remoteAddress\n                ) as Int\n            }\n\n            if (uid != Process.INVALID_UID) {\n                return uid\n            }\n        } catch (e: Exception) {\n            Log.w(\"ProcessInfoManager\", \"Exception in getProcessInfoUid\", e)\n            return null\n        }\n\n        Log.w(\n            \"ProcessInfoManager\",\n            \"Failed to get UID for local address $localAddress and remote address $remoteAddress\"\n        )\n        return null\n    }\n\n    suspend fun getProcessInfoByPort(host: String?, localPort: Int): ProcessInfo? {\n        val networkInfo = localPortCache.get(localPort)\n        if (networkInfo != null) {\n            val processInfo = getProcessInfo(networkInfo.uid)\n            if (processInfo != null) {\n                val result = processInfo.copy()\n                result[\"remoteHost\"] = networkInfo.remoteHost\n                result[\"remotePort\"] = networkInfo.remotePort\n                return result\n            }\n            return null\n        }\n\n        if (host == null || localPort <= 0 || ProxyVpnService.host == null || ProxyVpnService.port <= 0) {\n            Log.w(\"ProcessInfoManager\", \"Invalid host or local port: $host:$localPort or ProxyVpnService not initialized\")\n            return null\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n            return withContext(Dispatchers.IO) {\n                val localAddress = InetSocketAddress(host, localPort)\n                val remoteAddress = InetSocketAddress(ProxyVpnService.host, ProxyVpnService.port)\n\n                val uid = getProcessInfoUid(localAddress, remoteAddress)\n\n                if (uid == null || uid == Process.INVALID_UID) {\n                    return@withContext null\n                }\n\n\n                val processInfo = getProcessInfo(uid)\n                if (processInfo != null) {\n                    localPortCache.put(\n                        localPort, NetworkInfo(uid, remoteAddress.hostString, remoteAddress.port)\n                    )\n\n                    val result = processInfo.copy()\n                    result[\"remoteHost\"] = remoteAddress.hostString\n                    result[\"remotePort\"] = remoteAddress.port\n                    return@withContext result\n                } else {\n                    Log.w(\"ProcessInfoManager\", \"No process info found for UID: $uid\")\n                    null\n                }\n            }\n        } else {\n            Log.w(\"ProcessInfoManager\", \"Access to /proc/net/tcp is restricted on non-rooted devices.\")\n        }\n        return null\n    }\n\n    fun getRemoteAddressByPort(localPort: Int): Map<String, Any>? {\n        val networkInfo = localPortCache.get(localPort)\n        if (networkInfo != null) {\n            return mapOf(\n                \"remoteHost\" to networkInfo.remoteHost,\n                \"remotePort\" to networkInfo.remotePort\n            )\n        }\n        return null\n    }\n\n    private fun getProcessInfo(uid: Int): ProcessInfo? {\n        var appInfo = appInfoCache.get(uid)\n        if (appInfo != null) return appInfo\n\n        val packageManager = activity?.packageManager ?: return null\n        val pkgNames: Array<String>? = try {\n            packageManager.getPackagesForUid(uid)\n        } catch (e: Exception) {\n            Log.w(\"ProcessInfoManager\", \"getPackagesForUid SecurityException: $uid\", e)\n            null\n        }\n        if (pkgNames == null) return null\n\n        for (pkgName in pkgNames) {\n            try {\n                val applicationInfo = packageManager.getApplicationInfo(pkgName, 0)\n                appInfo = ProcessInfo.create(packageManager, applicationInfo)\n                appInfoCache.put(uid, appInfo)\n                return appInfo\n            } catch (e: Exception) {\n                // Ignore packages that can't be found\n            }\n        }\n        return null\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/util/SimpleCache.kt",
    "content": "package com.network.proxy.vpn.util\n\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.Executors\nimport java.util.concurrent.TimeUnit\n\nclass SimpleCache<K, V>(\n    private val maxSize: Int,\n    private val expireAfterAccess: Long,\n    private val timeUnit: TimeUnit\n) {\n    private val cache = ConcurrentHashMap<K, CacheEntry<V>>()\n\n    companion object {\n        private val EXECUTOR = Executors.newSingleThreadScheduledExecutor()\n    }\n\n\n    init {\n        EXECUTOR.scheduleWithFixedDelay(\n            { cleanUp() },\n            expireAfterAccess,\n            expireAfterAccess,\n            timeUnit\n        )\n    }\n\n    fun put(key: K, value: V) {\n        if (cache.size >= maxSize) {\n            cache.keys.iterator().next()?.let { cache.remove(it) }\n        }\n        cache[key] = CacheEntry(value, System.nanoTime())\n    }\n\n    fun get(key: K): V? {\n        val entry = cache[key] ?: return null\n        if (System.nanoTime() - entry.lastAccessTime > timeUnit.toNanos(expireAfterAccess)) {\n            cache.remove(key)\n            return null\n        }\n\n        entry.lastAccessTime = System.nanoTime()\n        return entry.value\n    }\n\n    fun remove(key: K) {\n        cache.remove(key)\n    }\n\n    fun clear() {\n        cache.clear()\n    }\n\n    private fun cleanUp() {\n        val now = System.nanoTime()\n        val expirationTime = timeUnit.toNanos(expireAfterAccess)\n\n        val iterator = cache.entries.iterator()\n        while (iterator.hasNext()) {\n            val entry = iterator.next()\n            if (now - entry.value.lastAccessTime > expirationTime) {\n                iterator.remove()\n            }\n        }\n    }\n\n    private data class CacheEntry<V>(val value: V, var lastAccessTime: Long)\n}"
  },
  {
    "path": "android/app/src/main/kotlin/com/network/proxy/vpn/util/TLS.kt",
    "content": "package com.network.proxy.vpn.util\n\nimport java.nio.ByteBuffer\nimport kotlin.math.min\n\n\nobject TLS {\n\n    /**\n     * 判断是否是TLS Client Hello\n     */\n    fun isTLSClientHello(packetData: ByteBuffer): Boolean {\n        if (packetData.remaining() < 43) return false\n        val position = packetData.position()\n        val data = packetData.array()\n        if (data[position].toInt() != 0x16 /* handshake */) return false\n        if (data[1 + position].toInt() != 0x03) return false\n        return if (data[5 + position].toInt() != 0x01) false else data[9 + position].toInt() == 0x03 && data[10 + position] >= 0x00 && data[1 + position] <= 0x03\n    }\n\n    /**\n     * 从TLS Client Hello 解析域名\n     */\n    fun getDomain(buffer: ByteBuffer): String? {\n        var offset = buffer.position()\n        val limit = buffer.limit()\n        //TLS Client Hello\n        if (buffer[offset].toInt() != 0x16) return null\n        //Skip 43 byte header\n        offset += 43\n        if (limit < (offset + 1)) return null\n\n        //read session id\n        val sessionIDLength = buffer[offset++]\n        offset += sessionIDLength\n\n        //read cipher suites\n        if (offset + 2 > limit) return null\n        val cipherSuitesLength = buffer.getShort(offset)\n        offset += 2\n        offset += cipherSuitesLength\n\n        //read Compression method.\n        if (offset + 1 > limit) return null\n        val compressionMethodLength = buffer[offset++].toInt() and 0xFF\n        offset += compressionMethodLength\n        if (offset > limit) return null\n\n        //read Extensions\n        if (offset + 2 > limit) return null\n\n        val extensionsLength = buffer.getShort(offset)\n        offset += 2\n        if (offset + extensionsLength > limit) return null\n\n        var end: Int = offset + extensionsLength\n        end = min(end, limit)\n        while (offset + 4 <= end) {\n            val extensionType = buffer.getShort(offset)\n            val extensionLength = buffer.getShort(offset + 2)\n            offset += 4\n            //server_name\n            if (extensionType.toInt() == 0) {\n                if (offset + 5 > limit) return null\n                val serverNameListLength = buffer.getShort(offset)\n                offset += 2\n                if (offset > limit) return null\n                if (offset + serverNameListLength > limit) return null\n\n                val serverNameType = buffer[offset++]\n                val serverNameLength = buffer.getShort(offset)\n                offset += 2\n                if (offset > limit || serverNameType.toInt() != 0) return null\n                if (offset + serverNameLength > limit) return null\n                val serverNameBytes = ByteArray(serverNameLength.toInt())\n                buffer.get(serverNameBytes)\n                return String(serverNameBytes)\n            } else {\n                offset += extensionLength\n            }\n        }\n        return null\n    }\n\n}"
  },
  {
    "path": "android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "android/app/src/main/res/drawable-v21/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"?android:colorBackground\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\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=\"@mipmap/ic_launcher_background\"/>\n  <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n  <monochrome android:drawable=\"@mipmap/ic_launcher_monochrome\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n\n    <string name=\"vpn_active_notification_title\">ProxyPin Active</string>\n    <string name=\"vpn_active_notification_content\">抓包正在运行</string>\n\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "android/build.gradle",
    "content": "allprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    subprojects {\n        afterEvaluate { project ->\n            if (project.hasProperty('android')) {\n                project.android {\n                    if (namespace == null) {\n                        namespace project.group\n                    }\n                }\n            }\n        }\n    }\n}\n\nrootProject.buildDir = '../build'\nsubprojects {\n    afterEvaluate { project ->\n        if (project.extensions.findByName(\"android\") != null) {\n            Integer pluginCompileSdk = project.android.compileSdk\n            if (pluginCompileSdk != null && pluginCompileSdk < 31) {\n                project.logger.error(\n                        \"Warning: Overriding compileSdk version in Flutter plugin: \"\n                                + project.name\n                                + \" from \"\n                                + pluginCompileSdk\n                                + \" to 31 (to work around https://issuetracker.google.com/issues/199180389).\"\n                                + \"\\nIf there is not a new version of \" + project.name + \", consider filing an issue against \"\n                                + project.name\n                                + \" to increase their compileSdk to the latest (otherwise try updating to the latest version).\"\n                )\n                project.android {\n                    compileSdk 31\n                }\n            }\n        }\n    }\n}\n\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n}\nsubprojects {\n    project.evaluationDependsOn(':app')\n}\n\ntasks.register(\"clean\", Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "#Tue Nov 28 00:35:45 CST 2023\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-all.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx4G\nandroid.useAndroidX=true\nandroid.enableJetifier=true\nandroid.nonFinalResIds=false\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "import java.util.Properties\n\npluginManagement {\n    def flutterSdkPath = {\n        def properties = new Properties()\n        file(\"local.properties\").withInputStream { properties.load(it) }\n        def flutterSdkPath = properties.getProperty(\"flutter.sdk\")\n        assert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\n        return flutterSdkPath\n    }\n    settings.ext.flutterSdkPath = flutterSdkPath()\n\n    includeBuild(\"${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle\")\n\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\n\nplugins {\n    id \"dev.flutter.flutter-plugin-loader\" version \"1.0.0\"\n    id \"com.android.application\" version '8.9.1' apply false\n    id \"org.jetbrains.kotlin.android\" version \"2.1.0\" apply false\n}\n\ninclude \":app\"\n"
  },
  {
    "path": "assets/certs/ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIID4jCCAsqgAwIBAgIJAKcH8Dna4mnZMA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV\nBAYTAkNOMQswCQYDVQQIDAJCSjEQMA4GA1UEBwwHQmVpSmluZzERMA8GA1UECgwI\nUHJveHlQaW4xETAPBgNVBAsMCFByb3h5UGluMRQwEgYDVQQDDAtQcm94eVBpbiBD\nQTAeFw0yMzA2MjQxNjA2MDlaFw0zMzA2MjExNjA2MTBaMGgxCzAJBgNVBAYTAkNO\nMQswCQYDVQQIDAJCSjEQMA4GA1UEBwwHQmVpSmluZzERMA8GA1UECgwIUHJveHlQ\naW4xETAPBgNVBAsMCFByb3h5UGluMRQwEgYDVQQDDAtQcm94eVBpbiBDQTCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRjfFvFDZS+PsdedUNq0Kn5t7RF\nNS0iQrZALr4LJm3UwtatHtMEWBb9ptam8pWezxrZPZ81+qnTcaq/To82yus5hJa4\nJRk223YWn5JDd4izH4gcnSomhUQ6Ycrc0v+I7UEaHV+bQsleHEfYi2+E1qF+FBhR\nveLSPmz2QORd/U4+gDlOptgNWMQ9OTRHsMoDzb8J4SlcBu+s0dnq2WHOM9boGnfk\n2wIgE+16uB23epPoYjex8zYGUswh8gNrIzXsr7i9IGtGf67FQYCWOXfZLeGgy0Q0\n/r1lwSmywUkNaZIsiGZHveZsLtW93MWMFw0uneEvHsuQV+e8sdLI/633TGcCAwEA\nAaOBjjCBizAdBgNVHQ4EFgQU4YXwKkBDFoZY3D81RM25ECSc2qcwHwYDVR0jBBgw\nFoAU4YXwKkBDFoZY3D81RM25ECSc2qcwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B\nAf8EBAMCAaYwEwYDVR0RBAwwCoIIUHJveHlQaW4wEwYDVR0lBAwwCgYIKwYBBQUH\nAwEwDQYJKoZIhvcNAQELBQADggEBAAc2s5TwuOdPdl3gYs121sY+HEMyXfsnVTGV\ndIlgjf+a0ECir2bcs64udaCIgBjd/vqhShMeqeQ4GJW7Ypb9556L213xjbLj/ZVU\nrgZda6oVd4der8YEHXKLxWAGlZQeeKHhw1lN4PYwxxGaf7/wsM7Dil0JLyOBtJaJ\nzNRzVzK9UHASDx0qDQVUBbeYzRviVCjxAGBNM/eNlPDX7m//vgCLxQgcxVdoJvMQ\nkSVQddo+d8fxnPAVx77dyX0T/ek7PQOsL6d08TVCdvgv50LwE8f9EMhHVv7zjEv2\n0ZSaRQ0pvUnc0ClKXIGeMD71eYeeTz7CGjndxy5bdV/wmoo3Yek=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "assets/certs/ca_key.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEY3xbxQ2Uvj7H\nXnVDatCp+be0RTUtIkK2QC6+CyZt1MLWrR7TBFgW/abWpvKVns8a2T2fNfqp03Gq\nv06PNsrrOYSWuCUZNtt2Fp+SQ3eIsx+IHJ0qJoVEOmHK3NL/iO1BGh1fm0LJXhxH\n2ItvhNahfhQYUb3i0j5s9kDkXf1OPoA5TqbYDVjEPTk0R7DKA82/CeEpXAbvrNHZ\n6tlhzjPW6Bp35NsCIBPtergdt3qT6GI3sfM2BlLMIfIDayM17K+4vSBrRn+uxUGA\nljl32S3hoMtENP69ZcEpssFJDWmSLIhmR73mbC7VvdzFjBcNLp3hLx7LkFfnvLHS\nyP+t90xnAgMBAAECggEAPYnPFhKRRuK2WVLH+/Akop6Vae+l0hbCQMmr2/EygYgB\n5bMpzYW29L1W4jw+F5RD4W3hWVpYyY5wN8jqnQXWYA8N9QyO02/VJRPBvNtXQYaf\ngs80kFixucdxjVfU5i3J6nR8b9D/BIpw4jKAvtkpSEFxmo1CqyimVw5zFxw8m599\ngJX/WeA88l9/4/tGQ24TAZV7OaP+jgqb4hOPC6gB5YHYnGFfuAh8q1Gf3wGnU+3/\npdEDq1UPvMwZ3J7ifTjMHYh2gnT/xQSxOddbtNJfaBx8fUFC4GVEEZ1+j0zc2bOp\n/7q+Ab0QXLjMbe3bftMZZqffp2X6NLJipLQcw/HRQQKBgQDonHdqt5ZcsF7+Vsl4\nKwmMNAz+jO6j51LU60F7QDhk7hGkCvUF2zJgYSkjlUNl6VS5aGmWTyUR3T3Eqiqs\nr81Qao5mxF0MjUU2QKgsw57YG2yASgSPdGqW0PFu1yrxLS51qLIGbp5AuZLULO+M\niTvO1SRm29q45F9f/m0NRda0TQKBgQDYIqGVFcyQvQzGPZc5iOI9we526p+MGEsa\nysRHs8wXJKCiINH2iw1bJGyRCOIZyFQwMRteC174tRnyZpsgTu6wTgaVnTHS8ZNQ\nLfjAQsMbs7TItjQF88/thujP15BXzTN7HN1y5kOVCAI7EvLJlZ4jMewfj+aqv2Sb\no5ungsWtgwKBgQCgo2WIqk5JpneDt9WcikQmsc+DfzpSsK6wYeMvxbLsaMh//B0o\nNS8+BftOGoeX+qJLBINejTuxcZN1nHqqFSJ59YxwBg2oXGs+wzog59trrMyqb/Nk\nSmZNzu/ctvVt5uDd2mlPLddWJZHzuzCXYjKObP2dlxkedIA1H9SZxPA4RQKBgDAS\n29/ePmb/NcUuU+GfObtE1HaszxoJGUN3UFsmecG4Cuak6C6vVSQtoNxNnoTfkyI4\n+f5cBx7IoWHSQrTX+a1LXZmPolJqGzsdTpPtBZq2yQJPzJh6V4hclpIMP3XYFZhP\nnk39O5D9fAmJuGjwF4F6jCulBUh7U7RumqOSqcdjAoGBAKxCtQ0XT0Rlc6B37xTK\n/fVYaVbSDISBSVYJTy5vjQi5z+bqUaQrmfeW1z+WoVTeP0ZUgcxTXJbPBVeAC8Wx\noTYfh5yTEu8FCBpBSWWsCteodBBZxXpINLuk9Ex44yxvuFhulugmYzyga+nqufV/\nN5e8NEl7aISBW+PK16pnNO0e\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "assets/js/fetch.js",
    "content": "function fetch(url, options) {\n    options = options || {};\n    return new Promise((resolve, reject) => {\n        const request = new XMLHttpRequest();\n        const keys = [];\n        const all = [];\n        const headers = {};\n\n        const response = () => ({\n            ok: (request.status / 100 | 0) === 2,\t\t// 200-299\n            statusText: request.statusText,\n            status: request.status,\n            url: request.responseURL,\n            body: request.response.body,\n            text: () => Promise.resolve(request.responseText),\n            json: () => {\n                // TODO: review this handle because it may discard \\n from json attributes\n                try {\n                    // console.log('RESPONSE TEXT IN FETCH: ' + request.responseText);\n                    return Promise.resolve(JSON.parse(request.responseText));\n                } catch (e) {\n                    // console.log('ERROR on fetch parsing JSON: ' + e.message);\n                    return Promise.resolve(request.responseText);\n                }\n            },\n\n            blob: () => Promise.resolve(request.response.body),\n            clone: response,\n            headers: {\n                ...headers,\n                keys: () => keys,\n                entries: () => all,\n                get: n => headers[n.toLowerCase()],\n                has: n => n.toLowerCase() in headers,\n            }\n        });\n\n        request.open(options.method || 'get', url, true);\n\n        request.onload = () => {\n            request.getAllResponseHeaders().replace(/^(.*?):[^\\S\\n]*([\\s\\S]*?)$/gm, (m, key, value) => {\n                keys.push(key = key.toLowerCase());\n                all.push([key, value]);\n                headers[key] = headers[key] ? `${headers[key]},${value}` : value;\n            });\n            resolve(response());\n        };\n\n        request.onerror = reject;\n\n        request.withCredentials = options.credentials == 'include';\n\n        if (options.headers) {\n            if (options.headers.constructor.name == 'Object') {\n                for (const i in options.headers) {\n                    request.setRequestHeader(i, options.headers[i]);\n                }\n            } else { // if it is some Headers pollyfill, the way to iterate is through for of\n                for (const header of options.headers) {\n                    request.setRequestHeader(header[0], header[1]);\n                }\n            }\n        }\n\n        request.send(options.body || null);\n    });\n}"
  },
  {
    "path": "devtools_options.yaml",
    "content": "description: This file stores settings for Dart & Flutter DevTools.\ndocumentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states\nextensions:\n"
  },
  {
    "path": "distribute_options.yaml",
    "content": "output: dist/\n\nreleases:\n  - name: release\n    jobs:\n      - name: macos-dmg\n        package:\n          platform: macos\n          target: dmg\n          build_args:\n            profile: true\n\n      - name: windows-exe\n        package:\n          platform: windows\n          target: exe\n          build_args:\n            profile: true\n\n      - name: windows-msix\n        package:\n          platform: windows\n          target: msix\n\n      - name: windows-zip\n        package:\n          platform: windows\n          target: zip\n"
  },
  {
    "path": "ios/.gitignore",
    "content": "**/dgph\n*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/ephemeral/\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "ios/Flutter/AppFrameworkInfo.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  <key>CFBundleDevelopmentRegion</key>\n  <string>en</string>\n  <key>CFBundleExecutable</key>\n  <string>App</string>\n  <key>CFBundleIdentifier</key>\n  <string>io.flutter.flutter.app</string>\n  <key>CFBundleInfoDictionaryVersion</key>\n  <string>6.0</string>\n  <key>CFBundleName</key>\n  <string>App</string>\n  <key>CFBundlePackageType</key>\n  <string>FMWK</string>\n  <key>CFBundleShortVersionString</key>\n  <string>1.0</string>\n  <key>CFBundleSignature</key>\n  <string>????</string>\n  <key>CFBundleVersion</key>\n  <string>1.0</string>\n  <key>MinimumOSVersion</key>\n  <string>13.0</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Flutter/Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Flutter/Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Podfile",
    "content": "# Uncomment this line to define a global platform for your project\nplatform :ios, '13.0'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_ios_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n  \n  pod 'SnapKit', '~> 5.0.1'\n  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\n  target 'RunnerTests' do\n    inherit! :search_paths\n  end\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_ios_build_settings(target)\n\n    target.build_configurations.each do |config|\n      # You can remove unused permissions here\n      # for more information: https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h\n      # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'\n      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [\n        '$(inherited)',\n        ## dart: PermissionGroup.camera\n        'PERMISSION_CAMERA=1',\n        ## dart: PermissionGroup.photos\n       # 'PERMISSION_PHOTOS=1',\n      ]\n    end\n  end\nend\n"
  },
  {
    "path": "ios/ProxyPin/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>NSExtension</key>\n\t<dict>\n\t\t<key>NSExtensionPointIdentifier</key>\n\t\t<string>com.apple.networkextension.packet-tunnel</string>\n\t\t<key>NSExtensionPrincipalClass</key>\n\t\t<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/ProxyPin/PacketTunnelProvider.swift",
    "content": "//\n//  PacketTunnelProvider.swift\n//  ProxyPin\n//\n//  Created by 汪红恩 on 2023/7/4.\n//\n\nimport NetworkExtension\nimport Network\nimport os.log\n\nclass PacketTunnelProvider: NEPacketTunnelProvider {\n    private var proxyVpnService: ProxyVpnService?\n    \n    override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {\n        NSLog(\"startTunnel\")\n\n        guard let conf = (protocolConfiguration as! NETunnelProviderProtocol).providerConfiguration else{\n            NSLog(\"[ERROR] No ProtocolConfiguration Found\")\n            exit(EXIT_FAILURE)\n        }\n\n        let host = conf[\"proxyHost\"] as! String\n        let proxyPort = conf[\"proxyPort\"] as! Int\n        let ipProxy = conf[\"ipProxy\"] as! Bool? ?? false\n\n        // parse proxyPassDomains: accept either [String] or comma-separated String\n        var proxyPassDomains: [String]? = nil\n        if let arr = conf[\"proxyPassDomains\"] as? [String] {\n            proxyPassDomains = arr.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }\n        } else if let csv = conf[\"proxyPassDomains\"] as? String {\n            let list = csv.components(separatedBy: \",\").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }\n            proxyPassDomains = list.isEmpty ? nil : list\n        }\n\n//        let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: \"127.0.0.1\")\n        let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: host)\n        NSLog(conf.debugDescription)\n     \n        networkSettings.mtu = 1500\n        \n        let ipv4Settings = NEIPv4Settings(addresses: [\"10.0.0.2\"], subnetMasks: [\"255.255.255.255\"])\n       \n        if (ipProxy){\n            ipv4Settings.includedRoutes = [NEIPv4Route.default()]\n           ipv4Settings.excludedRoutes = [\n               NEIPv4Route(destinationAddress: \"10.0.0.0\", subnetMask: \"255.0.0.0\"),\n               NEIPv4Route(destinationAddress: \"100.64.0.0\", subnetMask: \"255.192.0.0\"),\n//                NEIPv4Route(destinationAddress: \"127.0.0.0\", subnetMask: \"255.0.0.0\"),\n               NEIPv4Route(destinationAddress: \"169.254.0.0\", subnetMask: \"255.255.0.0\"),\n               NEIPv4Route(destinationAddress: \"172.16.0.0\", subnetMask: \"255.240.0.0\"),\n               NEIPv4Route(destinationAddress: \"192.168.0.0\", subnetMask: \"255.255.0.0\"),\n               NEIPv4Route(destinationAddress: \"17.0.0.0\", subnetMask: \"255.0.0.0\"),\n           ]\n            \n           let dns = \"223.5.5.5,8.8.8.8\"\n           let dnsSettings = NEDNSSettings(servers: dns.components(separatedBy: \",\"))\n           dnsSettings.matchDomains = [\"\"]\n           dnsSettings.matchDomainsNoSearch = true\n           networkSettings.dnsSettings = dnsSettings\n        }\n        \n        //http代理\n        let proxySettings = NEProxySettings()\n        proxySettings.httpEnabled = true\n        proxySettings.httpServer = NEProxyServer(address: host, port: proxyPort)\n        proxySettings.httpsEnabled = true\n        proxySettings.httpsServer = NEProxyServer(address: host, port: proxyPort)\n        // If a proxyPassDomains list was provided, use it as the exceptionList so these domains bypass the proxy.\n        if let pass = proxyPassDomains {\n            proxySettings.exceptionList = pass\n        } \n\n        proxySettings.matchDomains = [\"\"]\n        networkSettings.proxySettings =  proxySettings\n\n        networkSettings.ipv4Settings = ipv4Settings\n        \n        setTunnelNetworkSettings(networkSettings) { error in\n           guard error == nil else {\n               NSLog(\"startTunnel Encountered an error setting up the network: \\(error.debugDescription)\")\n               completionHandler(error)\n               return\n           }\n\n           if (ipProxy){\n             let proxyAddress =  Network.NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: UInt16(proxyPort))!)\n             self.proxyVpnService = ProxyVpnService(packetFlow: self.packetFlow, proxyAddress: proxyAddress)\n             self.proxyVpnService!.start()\n           }\n           completionHandler(nil)\n       }\n    }\n\n    override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {\n        proxyVpnService?.stop()\n        completionHandler()\n    }\n\n    override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {\n        // Add code here to handle the message.\n        if let handler = completionHandler {\n            NSLog(\"handleAppMessage \", messageData.debugDescription)\n            handler(messageData)\n        }\n    }\n\n    override func sleep(completionHandler: @escaping () -> Void) {\n        // Add code here to get ready to sleep.\n        completionHandler()\n    }\n\n    override func wake() {\n        // Add code here to wake up.\n    }\n}\n"
  },
  {
    "path": "ios/ProxyPin/ProxyPin-Bridging-Header.h",
    "content": "//\n//  ProxyPin-Bridging-Header.h\n//  Runner\n//\n//  Created by wanghongen on 2025/5/28.\n//\n\n#import \"GBPing.h\"\n"
  },
  {
    "path": "ios/ProxyPin/ProxyPin.entitlements",
    "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    <key>com.apple.security.application-groups</key>\n    <array>\n        <string>group.com.proxy.pin</string>\n    </array>\n    <key>com.apple.developer.networking.networkextension</key>\n    <array>\n        <string>packet-tunnel-provider</string>\n    </array>\n    <key>com.apple.developer.networking.vpn.api</key>\n    <array>\n        <string>allow-vpn</string>\n    </array>\n\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/ProxyPin/vpn/Connection.swift",
    "content": "//\n//  Connection.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/17.\n//\n\nimport Foundation\n\n\nimport Foundation\nimport Network\nimport os.log\n\nclass Connection{\n    var nwProtocol: NWProtocol\n    var sourceIp: UInt32\n    var sourcePort: UInt16\n    var destinationIp: UInt32\n    var destinationPort: UInt16\n    var channel: NWConnection?\n    \n    var isInitConnect: Bool = false\n    var isConnected: Bool = false\n    var isClosingConnection: Bool = false\n    var isAbortingConnection: Bool = false\n    var isAckedToFin: Bool = false\n\n    private let connectionCloser: ConnectionManager\n\n    init(nwProtocol: NWProtocol, sourceIp: UInt32, sourcePort: UInt16, destinationIp: UInt32, destinationPort: UInt16, connectionCloser: ConnectionManager) {\n        self.nwProtocol = nwProtocol\n        self.sourceIp = sourceIp\n        self.sourcePort = sourcePort\n        self.destinationIp = destinationIp\n        self.destinationPort = destinationPort\n        self.connectionCloser = connectionCloser\n    }\n    \n    //发送缓冲区，用于存储要从vpn客户端发送到目标主机的数据\n    var sendBuffer = Data()\n\n    var hasReceivedLastSegment = false\n    \n    //从客户端接收的最后一个数据包\n    var lastIpHeader: IP4Header?\n    var lastTcpHeader: TCPHeader?\n    var lastUdpHeader: UDPHeader?\n    \n    var timestampSender = 0\n    var timestampReplyTo = 0\n    \n    //从客户端接收的序列\n    var recSequence: UInt32 = 0\n    \n    //在tcp选项内的SYN期间由客户端发送\n    var maxSegmentSize = 0\n    \n    //跟踪我们发送给客户端的ack，并等待客户端返回ack\n    var sendUnAck: UInt32 = 0\n    \n    //发送到客户端的下一个ack\n    var sendNext: UInt32 = 0\n    \n    static func getConnectionKey(nwProtocol: NWProtocol, destIp: UInt32, destPort: UInt16, sourceIp: UInt32, sourcePort: UInt16) -> String {\n        let destIpString = PacketUtil.intToIPAddress(destIp)\n        let sourceIpString = PacketUtil.intToIPAddress(sourceIp)\n        return \"\\(nwProtocol)|\\(sourceIpString):\\(sourcePort)->\\(destIpString):\\(destPort)\"\n    }\n\n    func closeConnection() {\n        connectionCloser.closeConnection(connection: self)\n    }\n    \n    func addSendData(data: Data) {\n       self.sendBuffer.append(data)\n\n        if (self.channel?.state != .ready) {\n           os_log(\"Connection %{public}@ is not ready, cannot send data\", log: OSLog.default, type: .debug, self.description)\n           return\n       }\n\n        self.sendToDestination()\n    }\n    \n    //发送到目标服务器的数据\n    func sendToDestination() {\n//         os_log(\"Sending data to destination key %{public}@\", log: OSLog.default, type: .debug, self.description)\n        if (self.sendBuffer.count == 0) {\n            return\n        }\n\n        let data = self.sendBuffer\n        self.sendBuffer.removeAll()\n\n        self.channel?.send(content: data, completion: .contentProcessed({ error in\n            if let error = error {\n                os_log(\"Failed to send data to destination key %{public}@ error: %{public}@\", log: OSLog.default, type: .error, self.description, error.localizedDescription)\n                self.closeConnection()\n            }\n        }))\n    }\n\n    var description: String {\n        return Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: destinationIp, destPort: destinationPort, sourceIp: sourceIp, sourcePort: sourcePort)\n    }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/ConnectionHandler.swift",
    "content": "//\n//  ConnectionHandler.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/16.\n//\n\nimport Foundation\nimport NetworkExtension\nimport Network\nimport os.log\n\n\nenum ProtocolType: UInt8 {\n      case icmp = 1, tcp = 6, udp = 17\n}\n\n/// Handles incoming packets and routes them to the appropriate connection.\nclass ConnectionHandler {\n    private let manager: ConnectionManager\n    private let writer: NEPacketTunnelFlow\n    private let ioService: SocketIOService\n    \n    init(manager: ConnectionManager, writer: NEPacketTunnelFlow, ioService: SocketIOService) {\n        self.manager = manager\n        self.writer = writer\n        self.ioService = ioService\n    }\n    \n    //Handle unknown raw IP packet data\n    public func handlePacket(packet: Data, version: NSNumber?) {\n        guard let ipHeader = IPPacketFactory.createIP4Header(data: packet) else {\n            os_log(\"Malformed IP packet\", log: OSLog.default, type: .error)\n            return\n        }\n\n        if ipHeader.ipVersion != 4 {\n            os_log(\"Unsupported IP version: %d\", log: OSLog.default, type: .error, ipHeader.ipVersion)\n            return\n        }\n\n//         os_log(\"Handling packet length:%d, protocolNumber: %d\", log: OSLog.default, type: .default, packet.count, ipHeader.protocolNumber)\n\n        var clientPacketData = packet.subdata(in: IPPacketFactory.IP4_HEADER_SIZE..<packet.count)\n\n        switch ipHeader.protocolNumber {\n            case ProtocolType.tcp.rawValue:\n                handleTCPPacket(packet: clientPacketData, ipHeader: ipHeader)\n                break\n            case ProtocolType.udp.rawValue:\n                handleUDPPacket(clientPacketData: clientPacketData, ipHeader: ipHeader)\n                break\n            case ProtocolType.icmp.rawValue:\n                handleICMPPacket(clientPacketData: &clientPacketData, ipHeader: ipHeader)\n                break\n            default:\n                os_log(\"Unsupported IP protocol: %d\", log: OSLog.default, type: .error, ipHeader.protocolNumber)\n        }\n    }\n\n    func synchronized(_ lock: AnyObject, closure: () -> Void) {\n//        objc_sync_enter(lock)\n        closure()\n//        objc_sync_exit(lock)\n    }\n    \n    private func handleUDPPacket(clientPacketData: Data, ipHeader: IP4Header) {\n        guard let udpHeader = UDPPacketFactory.createUDPHeader(from: clientPacketData) else {\n            os_log(\"Malformed UDP packet\", log: OSLog.default, type: .error)\n            return\n        }\n        \n        var connection = manager.getConnection(\n            nwProtocol: .UDP,\n            ip: ipHeader.destinationIP,\n            port: udpHeader.destinationPort,\n            srcIp: ipHeader.sourceIP,\n            srcPort: udpHeader.sourcePort\n        )\n        \n        let newSession = connection == nil\n        if connection == nil {\n            connection = manager.createUDPConnection(\n                ip: ipHeader.destinationIP,\n                port: udpHeader.destinationPort,\n                srcIp: ipHeader.sourceIP,\n                srcPort: udpHeader.sourcePort\n            )\n        }\n        \n        \n        guard let connection = connection else {\n            os_log(\"Failed to create UDP connection\", log: OSLog.default, type: .error)\n            return\n        }\n\n        synchronized(connection) {\n            os_log(\"handle UDP Packet %{public}@\", log: OSLog.default, type: .default, connection.description)\n            if newSession {\n                ioService.registerSession(connection: connection)\n            }\n\n            let payload = clientPacketData.subdata(in: UDPPacketFactory.UDP_HEADER_LENGTH..<clientPacketData.count)\n\n            if ((payload.count + UDPPacketFactory.UDP_HEADER_LENGTH) != udpHeader.length) {\n                os_log(\"UDP  %{public}@  packet length mismatch: expected %d, got %d\", log: OSLog.default, type: .error, connection.description, udpHeader.length, payload.count)\n            }\n//             os_log(\"Received UDP packet\", log: OSLog.default, type: .default)\n            connection.lastIpHeader = ipHeader\n            connection.lastUdpHeader = udpHeader\n            manager.addClientData(data: payload, connection: connection)\n        }\n        manager.keepSessionAlive(connection: connection)\n    }\n    \n    func printByteArray(_ byteArray: Data) {\n        let byteArrayString = byteArray.map { String( format: \"0x%02X\",$0) }.joined(separator: \",\")\n        os_log(\"Packet data: %{public}@\", log: OSLog.default, type: .default, byteArrayString)\n    }\n    \n    private func handleTCPPacket(packet: Data, ipHeader: IP4Header) {\n        guard let tcpHeader = TCPPacketFactory.createTCPHeader(data: packet) else {\n            os_log(\"Malformed TCP packet\", log: OSLog.default, type: .error)\n            return\n        }\n        \n        let dataLength = tcpHeader.payload?.count ?? 0\n        let sourceIP = ipHeader.sourceIP\n        let destinationIP = ipHeader.destinationIP\n        let sourcePort = tcpHeader.sourcePort\n        let destinationPort = tcpHeader.destinationPort\n\n        let key = Connection.getConnectionKey(nwProtocol: .TCP, destIp: destinationIP, destPort: destinationPort, sourceIp: sourceIP, sourcePort: sourcePort)\n        \n        if (tcpHeader.isSYN()) {\n            os_log(\"Received SYN packet %{public}@ seq:%u\", log: OSLog.default, type: .default, key, tcpHeader.sequenceNumber)\n            // 3-way handshake + create new session\n            replySynAck(ipHeader: ipHeader, tcpHeader: tcpHeader)\n        } else if (tcpHeader.isACK()) {\n//            os_log(\"Received ACK packet for key: %{public}@\", log: OSLog.default, type: .debug, key)\n\n            guard let connection = manager.getConnectionByKey(key: key) else {\n                os_log(\"Ack for unknown session: %{public}@\", log: OSLog.default, type: .default, key)\n                if tcpHeader.isFIN() {\n                    sendLastAck(ip: ipHeader, tcp: tcpHeader)\n               } else if !tcpHeader.isRST() {\n                   sendRstPacket(ip: ipHeader, tcp: tcpHeader, dataLength: dataLength)\n                }\n                return\n            }\n\n            synchronized(connection) {\n                connection.lastIpHeader = ipHeader\n                connection.lastTcpHeader = tcpHeader\n\n                if dataLength > 0 {\n//                     os_log(\"[ConnectionHandler] Received data packet %{public}@ length:%d seq:%u, ack:%u\", log: OSLog.default, type: .default, connection.description, dataLength, tcpHeader.sequenceNumber, tcpHeader.ackNumber)\n                    \n                    //init proxy\n                    self.initProxyConnect(packetData: tcpHeader.payload!, destinationIP: destinationIP, destinationPort: destinationPort, connection: connection)\n                  \n                    manager.addClientData(data: tcpHeader.payload!, connection: connection)\n                    sendAck(ipHeader: ipHeader, tcpHeader: tcpHeader, acceptedDataLength: dataLength, connection: connection)\n                } else {\n//                      os_log(\"[ConnectionHandler] Received ACK packet %{public}@ seq:%u, ack:%u\", log: OSLog.default, type: .default, connection.description, tcpHeader.sequenceNumber, tcpHeader.ackNumber)\n                }\n\n                acceptAck(tcpHeader: tcpHeader, connection: connection)\n                \n                if connection.isClosingConnection {\n                    sendFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: connection)\n                } else if connection.isAckedToFin && !tcpHeader.isFIN() {\n                    manager.closeConnection(nwProtocol: .TCP, ip: destinationIP, port: destinationPort, srcIp: sourceIP, srcPort: sourcePort)\n                }\n\n                //received the last segment of data from vpn client\n                if tcpHeader.isPSH() {\n                    // Tell the NIO thread to immediately send data to the destination\n                    pushDataToDestination(connection: connection, tcpHeader: tcpHeader)\n                } else if tcpHeader.isFIN() {\n                    //fin from vpn client is the last packet\n                    //ack it\n                    ackFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: connection)\n                } else if tcpHeader.isRST() {\n                    resetTCPConnection(ip: ipHeader, tcp: tcpHeader)\n                }\n\n                if !connection.isAbortingConnection {\n                    manager.keepSessionAlive(connection: connection)\n                }\n            }\n        } else if tcpHeader.isFIN() {\n            os_log(\"Received FIN packet %{public}@:%d seq:%u\", log: OSLog.default, type: .default, PacketUtil.intToIPAddress(destinationIP), destinationPort, tcpHeader.sequenceNumber)\n            //case client sent FIN without ACK\n            guard let connection = manager.getConnection(nwProtocol: .TCP, ip: destinationIP, port: destinationPort, srcIp: sourceIP, srcPort: sourcePort) else {\n                ackFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: nil)\n                return\n            }\n            \n            manager.keepSessionAlive(connection: connection)\n        } else if tcpHeader.isRST() {\n            os_log(\"Received RST packet %{public}@:%d seq:%u\", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(destinationIP), destinationPort, tcpHeader.sequenceNumber)\n            resetTCPConnection(ip: ipHeader, tcp: tcpHeader)\n        } else {\n            os_log(\"Unknown TCP flag\", log: OSLog.default, type: .error)\n        }\n    }\n\n    private func initProxyConnect(\n        packetData: Data,\n        destinationIP: UInt32,\n        destinationPort: UInt16,\n        connection: Connection\n    ) {\n        guard !connection.isInitConnect else {\n            return\n        }\n\n        connection.isInitConnect = true\n\n        let supportsProtocol = supportsProtocol(packetData: packetData)\n        \n        let endpoint: Network.NWEndpoint\n        if (supportsProtocol && manager.proxyAddress != nil) {\n            endpoint = manager.proxyAddress!\n        } else {\n            let ipString = PacketUtil.intToIPAddress(destinationIP)\n            endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(ipString), port: NWEndpoint.Port(rawValue: destinationPort)!)\n        }\n        \n        // 使用 TCP 协议\n        let parameters = NWParameters.tcp\n        let nwConnection = NWConnection(to: endpoint, using: parameters)\n        connection.channel = nwConnection\n        connection.isInitConnect = true\n        self.ioService.registerSession(connection: connection)\n    }\n\n    private let methods: [String] = [\n        \"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"HEAD\", \"OPTIONS\", \"TRACE\", \"CONNECT\", \"PROPFIND\", \"REPORT\"\n    ]\n\n    private func supportsProtocol(packetData: Data) -> Bool {\n        let position = packetData.startIndex\n        // 判断是否是 SSL 握手\n        if TLS.isTLSClientHello(packetData: packetData) {\n            return true\n        }\n\n        // 检查是否包含 HTTP 方法\n        for method in methods {\n            if packetData.count - position < method.count {\n                continue\n            }\n            let range = position..<(position + method.count)\n            if let substring = String(data: packetData.subdata(in: range), encoding: .utf8),\n               method.caseInsensitiveCompare(substring) == .orderedSame {\n                return true\n            }\n        }\n        return false\n    }\n\n    //set connection as aborting so that background worker will close it.\n    func resetTCPConnection(ip: IP4Header, tcp: TCPHeader) {\n        let session = manager.getConnection(nwProtocol: .TCP, ip: ip.destinationIP, port: tcp.destinationPort, srcIp: ip.sourceIP, srcPort: tcp.sourcePort)\n        if let session = session {\n            session.isAbortingConnection = true\n        }\n    }\n\n    func ackFinAck(ipHeader: IP4Header, tcpHeader: TCPHeader, connection: Connection?) {\n        let ackNumber = tcpHeader.sequenceNumber + 1\n        let seqNumber = tcpHeader.ackNumber\n        let finAckData = TCPPacketFactory.createFinAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber, seqToClient: seqNumber, isFin: true, isAck: true)\n        write(data: finAckData)\n//        os_log(\"Sent FIN-ACK packet ack# %{public}d, seq# %{public}d\", log: OSLog.default, type: .default, ackNumber, seqNumber)\n        if let connection = connection {\n            manager.closeConnection(connection: connection)\n        }\n    }\n\n    func pushDataToDestination(connection: Connection, tcpHeader: TCPHeader) {\n        connection.timestampReplyTo = tcpHeader.timeStampSender\n        connection.timestampSender = Int(Date().timeIntervalSince1970)\n    }\n    \n    func sendFinAck(ipHeader: IP4Header, tcpHeader: TCPHeader, connection: Connection) {\n        let ackNumber = tcpHeader.sequenceNumber\n        let seqNumber = tcpHeader.ackNumber\n        let finAckData = TCPPacketFactory.createFinAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber, seqToClient: seqNumber, isFin: true, isAck: false)\n        write(data: finAckData)\n\n        connection.sendNext = seqNumber + 1\n        connection.isClosingConnection = false\n    }\n    \n    //acknowledge a packet.\n    func acceptAck(tcpHeader: TCPHeader, connection: Connection) {\n        let isCorrupted = PacketUtil.isPacketCorrupted(tcpHeader: tcpHeader)\n\n        if isCorrupted {\n            os_log(\"Packet is corrupted\", log: OSLog.default, type: .error)\n        }\n\n        if (tcpHeader.sequenceNumber > connection.recSequence) {\n            connection.recSequence = tcpHeader.sequenceNumber\n       }\n\n        if tcpHeader.ackNumber >= connection.sendUnAck - 1 || tcpHeader.ackNumber == connection.sendNext {\n            connection.sendUnAck = tcpHeader.ackNumber\n\n            connection.timestampReplyTo = tcpHeader.timeStampSender\n            connection.timestampSender = Int(Date().timeIntervalSince1970)\n        } else {\n            os_log(\"%{public}@ Not accepting ack# %d, it should be: %d\", log: OSLog.default, type: .error, connection.description ,tcpHeader.ackNumber, connection.sendNext)\n            os_log(\"%{public}@ Previous sendUnAck: %d\", log: OSLog.default, type: .error, connection.description, connection.sendUnAck)\n        }\n    }\n    \n    func sendAckForDisorder(ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int) {\n        let ackNumber = tcpHeader.sequenceNumber + UInt32(acceptedDataLength)\n//        os_log(\"Sent disorder ack, ack# %{public}d\", log: OSLog.default, type: .debug, ackNumber)\n        let ackData = TCPPacketFactory.createResponseAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber)\n        write(data: ackData)\n    }\n    \n    func sendAck(ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int, connection: Connection) {\n       synchronized(connection) {\n            let ackNumber = (tcpHeader.sequenceNumber + UInt32(acceptedDataLength)) % UInt32.max\n            connection.recSequence = ackNumber\n            let ackData = TCPPacketFactory.createResponseAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber)\n            self.write(data: ackData)\n\n//             os_log(\"[ConnectionHandler] Sent ACK packet to client %{public}@ ack: %u\", log: OSLog.default, type: .default, connection.description, ackNumber)\n        }\n    }\n\n    private func sendLastAck(ip: IP4Header, tcp: TCPHeader) {\n        let data = TCPPacketFactory.createResponseAckData(ipHeader: ip, tcpHeader: tcp, ackToClient: tcp.sequenceNumber + 1)\n        self.write(data: data)\n        os_log(\"Sent last ACK Packet to client with dest => %{public}@:%{public}d\", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(ip.destinationIP), tcp.destinationPort)\n    }\n\n    private func sendRstPacket(ip: IP4Header, tcp: TCPHeader, dataLength: Int) {\n        let data = TCPPacketFactory.createRstData(ipHeader: ip, tcpHeader: tcp, dataLength: dataLength)\n        self.write(data: data)\n        os_log(\"Sent RST Packet to client with dest => %{public}@:%{public}d\", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(ip.destinationIP), tcp.destinationPort)\n    }\n    \n    //create a new client's session and SYN-ACK packet data to respond to client\n    private func replySynAck(ipHeader: IP4Header, tcpHeader: TCPHeader) -> Void {\n        ipHeader.identification = 0\n        let packet = TCPPacketFactory.createSynAckPacketData(ipHeader: ipHeader, tcpHeader: tcpHeader)\n\n        guard let tcpTransport = packet.transportHeader as? TCPHeader else {\n            os_log(\"Failed to extract TCP header from packet\", log: OSLog.default, type: .error)\n            return\n        }\n        \n        let connection = manager.createTCPConnection(\n            ip: ipHeader.destinationIP,\n            port: tcpHeader.destinationPort,\n            srcIp: ipHeader.sourceIP,\n            srcPort: tcpHeader.sourcePort\n        )\n        \n        if connection.lastIpHeader != nil {\n            resendAck(connection: connection)\n            return\n        }\n        \n        synchronized(connection) {\n            connection.maxSegmentSize = Int(tcpTransport.maxSegmentSize)\n            connection.sendUnAck = tcpTransport.sequenceNumber\n            connection.sendNext = tcpTransport.sequenceNumber + 1\n            \n            //client initial sequence has been incremented by 1 and set to ack\n            connection.recSequence = tcpTransport.ackNumber\n            connection.lastIpHeader = ipHeader\n            connection.lastTcpHeader = tcpHeader\n            if connection.isInitConnect {\n                self.ioService.registerSession(connection: connection)\n            }\n            self.write(data: packet.buffer)\n//             os_log(\"SYN-ACK %{public}@ packet length:%d sent ack:%u\", log: OSLog.default, type: .default, connection.description, packet.buffer.count, tcpTransport.ackNumber)\n        }\n    }\n\n    /**\n     * resend the last acknowledgment packet to VPN client, e.g. when an unexpected out of order\n     * packet arrives.\n     */\n    private func resendAck(connection: Connection) {\n        let data = TCPPacketFactory.createResponseAckData(\n            ipHeader: connection.lastIpHeader!,\n            tcpHeader: connection.lastTcpHeader!,\n            ackToClient: connection.recSequence\n        )\n//         os_log(\"Resending ACK packet %{public}@ ackToClient: %d\", log: OSLog.default, type: .default, connection.description, connection.recSequence)\n        self.write(data: data)\n    }\n\n\n    private func write(data: Data) {\n        self.writer.writePackets([data], withProtocols: [NSNumber(value: AF_INET)])\n    }\n\n    private func handleICMPPacket(clientPacketData: inout Data, ipHeader: IP4Header) {\n        guard let requestPacket = ICMPPacketFactory.parseICMPPacket(&clientPacketData) else {\n            os_log(\"Failed to parse ICMP packet\", log: OSLog.default, type: .error)\n            return\n        }\n\n//         os_log(\"Handling ICMP packet type: %d\", log: OSLog.default, type: .default, requestPacket.type)\n        if requestPacket.type == ICMPPacket.DESTINATION_UNREACHABLE_TYPE {\n             // This is a packet from the phone, telling somebody that a destination is unreachable.\n             // Might be caused by issues on our end, but it's unclear what kind of issues. Regardless,\n             // we can't send ICMP messages ourselves or react usefully, so we drop these silently.\n\n            return\n        } else if requestPacket.type != ICMPPacket.ECHO_REQUEST_TYPE {\n            // We only actually support outgoing ping packets. Loudly drop anything else:\n            os_log(\"Unknown ICMP type: %d\", log: OSLog.default, type: .error, requestPacket.type)\n            return\n        }\n\n          QueueFactory.instance.getQueue().async {\n              \n            if !self.isReachable(ipAddress: PacketUtil.intToIPAddress(ipHeader.destinationIP)) {\n                os_log(\"Failed ping, ignoring\", log: OSLog.default, type: .default)\n                return\n            }\n\n            let response = ICMPPacketFactory.buildSuccessPacket(requestPacket)\n\n            // Flip the address\n            let destination = ipHeader.destinationIP\n            let source = ipHeader.sourceIP\n            ipHeader.sourceIP = destination\n            ipHeader.destinationIP = source\n\n            let responseData = ICMPPacketFactory.packetToBuffer(ipHeader: ipHeader, packet: response)\n            os_log(\"Successful ping response\", log: OSLog.default, type: .default)\n            self.write(data: responseData)\n        }\n    }\n\n    private func isReachable(ipAddress: String) -> Bool {\n        do {\n            return true\n//            return try InetAddress.getByName(ipAddress).isReachable(timeout: 10000)\n        } catch {\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/ConnectionManager.swift",
    "content": "//\n//  ConnectionManager.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/16.\n//\n\nimport Foundation\nimport Network\nimport os.log\n\n//管理VPN客户端的连接\nclass ConnectionManager : CloseableConnection{\n    //static let instance = ConnectionManager()\n    \n    private var table: [String: Connection] = [:]\n\n    public var proxyAddress: NWEndpoint?\n    \n    private let defaultPorts: [UInt16] = [80, 443, 8080, 8088, 8888, 9000]\n    \n   \n    func getConnection(nwProtocol: NWProtocol, ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection? {\n        let key = Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort)\n        return getConnectionByKey(key: key)\n    }\n    \n    func getConnectionByKey(key: String) -> Connection? {\n        return table[key]\n    }\n    \n    func createTCPConnection(ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection {\n        let key = Connection.getConnectionKey(nwProtocol: .TCP, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort)\n\n        if let existingConnection = table[key] {\n            return existingConnection\n        }\n\n        let connection = Connection(nwProtocol: .TCP, sourceIp: srcIp, sourcePort: srcPort, destinationIp: ip, destinationPort: port, connectionCloser: self)\n\n        let ipString = PacketUtil.intToIPAddress(ip)\n\n        let endpoint: NWEndpoint\n        if (proxyAddress == nil || !defaultPorts.contains(port) || isPrivateIP(ipString)) {\n            endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(ipString), port: NWEndpoint.Port(rawValue: port)!)\n            // 使用 TCP 协议\n            let parameters = NWParameters.tcp\n            let nwConnection = NWConnection(to: endpoint, using: parameters)\n            connection.channel = nwConnection\n            connection.isInitConnect = true\n        }\n\n        self.table[key] = connection\n        os_log(\"Created TCP connection %{public}@\", log: OSLog.default, type: .default, key)\n\n        return connection\n    }\n\n    private func isPrivateIP(_ ip: String) -> Bool {\n        return ip.hasPrefix(\"10.\") ||\n               ip.hasPrefix(\"172.\") && (16...31).contains(Int(ip.split(separator: \".\")[1]) ?? -1) ||\n               ip.hasPrefix(\"192.168.\")\n    }\n\n    func createUDPConnection(ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection {\n        let key = Connection.getConnectionKey(nwProtocol: .UDP, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort)\n\n        if let existingConnection = table[key] {\n            return existingConnection\n        }\n\n       let connection = Connection(nwProtocol: .UDP, sourceIp: srcIp, sourcePort: srcPort, destinationIp: ip, destinationPort: port, connectionCloser: self)\n     \n        let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host((PacketUtil.intToIPAddress(ip))), port: NWEndpoint.Port(rawValue: port)!)\n\n        let nwConnection = NWConnection(to: endpoint, using: .udp)\n        connection.channel = nwConnection\n\n        os_log(\"Created UDP connection %{public}@\", log: OSLog.default, type: .default, key)\n        self.table[key] = connection\n\n        return connection\n    }\n    \n    func closeConnection(connection: Connection) {\n        closeConnection(\n            nwProtocol: connection.nwProtocol, ip: connection.destinationIp, port: connection.destinationPort,\n            srcIp: connection.sourceIp, srcPort: connection.sourcePort\n        )\n    }\n    \n    // 从内存中删除连接，然后关闭套接字。\n    func closeConnection(nwProtocol: NWProtocol, ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) {\n        let key = Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort)\n       \n        if let connection = self.table.removeValue(forKey: key) {\n            if connection.channel?.state != .cancelled {\n                connection.channel?.cancel()\n                os_log(\"Closed connection %{public}@\", log: OSLog.default, type: .debug, key)\n            } else {\n                os_log(\"Connection %{public}@ is already cancelled\", log: OSLog.default, type: .debug, key)\n            }\n        }\n    }\n    \n    //添加来自客户端的数据，该数据稍后将在接收到PSH标志时发送到目的服务器。\n    func addClientData(data: Data, connection: Connection)  {\n        guard data.count > 0 else {\n            return\n        }\n        \n        connection.addSendData(data: data)\n    }\n\n    func keepSessionAlive(connection: Connection) {\n        let key = Connection.getConnectionKey(\n            nwProtocol: connection.nwProtocol,\n            destIp: connection.destinationIp,\n            destPort: connection.destinationPort,\n            sourceIp: connection.sourceIp,\n            sourcePort: connection.sourcePort\n        )\n\n        self.table[key] = connection\n    }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/NWProtocol.swift",
    "content": "//\n//  NWProtocol.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/17.\n//\n\nimport Foundation\n\n\npublic enum NWProtocol {\n    case TCP,UDP\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/ProxyVpnService.swift",
    "content": "//\n//  ProxyService.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/17.\n//\n\nimport Foundation\nimport NetworkExtension\nimport Network\nimport os.log\n\nclass ProxyVpnService {\n\n    private var packetFlow: NEPacketTunnelFlow\n    private var connectionHandler: ConnectionHandler\n    private var socketIOService: SocketIOService\n    private var isRunning = true;\n    \n    init(packetFlow: NEPacketTunnelFlow, proxyAddress: Network.NWEndpoint?) {\n        self.packetFlow = packetFlow\n        self.socketIOService = SocketIOService(clientPacketWriter: packetFlow)\n        let manager = ConnectionManager()\n        manager.proxyAddress = proxyAddress\n        self.connectionHandler = ConnectionHandler(manager: manager, writer: packetFlow, ioService: socketIOService)\n    }\n    \n    \n    /**\n     Start processing packets, this should be called after registering all IP stacks.\n     \n     A stopped interface should never start again. Create a new interface instead.\n     */\n    func start() {\n        isRunning = true;\n        self.readPackets()\n    }\n\n    func stop() {\n        isRunning = false;\n        self.socketIOService.stop()\n    }\n    \n    func readPackets() -> Void {\n        if (!isRunning) {\n            return\n        }\n\n        self.packetFlow.readPackets { (packets, protocols) in\n            \n//             os_log(\"Read %d packets\", packets.count)\n            for (i, packet) in packets.enumerated() {\n                 self.connectionHandler.handlePacket(packet: packet, version: protocols[i])\n            }\n            self.readPackets()\n        }\n    }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/QueueFactory.swift",
    "content": "//\n//  QueueFactory.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/17.\n//\n\nimport Foundation\n\nclass QueueFactory {\n    static let instance = QueueFactory()\n\n    private let queue: DispatchQueue\n\n    private init() {\n        queue = DispatchQueue(label: \"com.network.ProxyPin.queue\")\n    }\n\n    func getQueue() -> DispatchQueue {\n        return queue\n    }\n\n    func executeAsync(block: @escaping () -> Void) {\n        queue.async {\n            block()\n        }\n    }\n\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/ping/GBPing.h",
    "content": "//\n//  GBPing.h\n//  GBPing\n//\n//  Created by Luka Mirosevic on 05/11/2012.\n//  Copyright (c) 2012 Goonbee. All rights reserved.\n//\n\n#import <Foundation/Foundation.h>\n\n#import \"GBPingSummary.h\"\n\n@class GBPingSummary;\n@protocol GBPingDelegate;\n\nNS_ASSUME_NONNULL_BEGIN\n\ntypedef void(^StartupCallback)(BOOL success, NSError * _Nullable error);\n\n@interface GBPing : NSObject\n\n@property (weak, nonatomic, nullable) id<GBPingDelegate>      delegate;\n\n@property (copy, nonatomic, nullable) NSString                *host;\n@property (assign, atomic) NSTimeInterval           pingPeriod;\n@property (assign, atomic) NSTimeInterval           timeout;\n@property (assign, atomic) NSUInteger               payloadSize;\n@property (assign, atomic) NSUInteger               ttl;\n@property (assign, atomic) NSUInteger               count;\n@property (assign, atomic, readonly) BOOL           isPinging;\n@property (assign, atomic, readonly) BOOL           isReady;\n@property (assign, atomic) BOOL                     useIpv4;\n@property (assign, atomic) BOOL                     useIpv6;\n\n@property (assign, atomic) BOOL                     debug;\n\n-(void)setupWithBlock:(StartupCallback)callback;\n-(void)startPinging;\n-(void)stop;\n\n@end\n\n@protocol GBPingDelegate <NSObject>\n\n@optional\n\n-(void)ping:(GBPing *)pinger didFinishWithTime:(NSTimeInterval)time;\n-(void)ping:(GBPing *)pinger didFailWithError:(NSError *)error;\n\n-(void)ping:(GBPing *)pinger didSendPingWithSummary:(GBPingSummary *)summary;\n-(void)ping:(GBPing *)pinger didFailToSendPingWithSummary:(GBPingSummary *)summary error:(NSError *)error;\n-(void)ping:(GBPing *)pinger didTimeoutWithSummary:(GBPingSummary *)summary;\n-(void)ping:(GBPing *)pinger didReceiveReplyWithSummary:(GBPingSummary *)summary;\n-(void)ping:(GBPing *)pinger didReceiveUnexpectedReplyWithSummary:(GBPingSummary *)summary;\n\n@end\n\nNS_ASSUME_NONNULL_END\n"
  },
  {
    "path": "ios/ProxyPin/vpn/ping/GBPing.m",
    "content": "//\n//  GBPing.m\n//  GBPing\n//\n//  Created by Luka Mirosevic on 05/11/2012.\n//  Copyright (c) 2012 Goonbee. All rights reserved.\n//\n\n#import \"GBPing.h\"\n\n#if TARGET_OS_EMBEDDED || TARGET_IPHONE_SIMULATOR\n    #import <CFNetwork/CFNetwork.h>\n#else\n    #import <CoreServices/CoreServices.h>\n#endif\n\n#import \"ICMPHeader.h\"\n\n#include <sys/socket.h>\n#include <netinet/in.h>\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n#include <errno.h>\n#include <string.h>\n#include <sys/types.h>\n#include <arpa/inet.h>\n#include <netdb.h>\n\nstatic NSTimeInterval const kPendingPingsCleanupGrace = 1.0;\n\nstatic NSUInteger const kDefaultPayloadSize =           56;\nstatic NSUInteger const kDefaultTTL =                   49;\nstatic NSTimeInterval const kDefaultPingPeriod =        1.0;\nstatic NSTimeInterval const kDefaultTimeout =           2.0;\n\n@interface GBPing ()\n\n@property (assign, atomic) int                          socket;\n@property (strong, nonatomic) NSData                    *hostAddress;\n@property (strong, nonatomic) NSString                  *hostAddressString;\n@property (assign, nonatomic) uint16_t                  identifier;\n@property (assign, nonatomic) NSUInteger                counter;\n\n@property (assign, atomic, readwrite) BOOL              isPinging;\n@property (assign, atomic, readwrite) BOOL              isReady;\n@property (assign, nonatomic) NSUInteger                nextSequenceNumber;\n@property (strong, atomic) NSMutableDictionary          *pendingPings;\n@property (strong, nonatomic) NSMutableDictionary       *timeoutTimers;\n\n@property (strong, nonatomic) dispatch_queue_t          setupQueue;\n\n@property (assign, atomic) BOOL                         isStopped;\n\n@end\n\n@implementation GBPing {\n    NSUInteger                                          _payloadSize;\n    NSUInteger                                          _ttl;\n    NSUInteger                                          _count;\n    NSTimeInterval                                      _timeout;\n    NSTimeInterval                                      _pingPeriod;\n    NSTimeInterval                                      _endTime;\n}\n\n#pragma mark - custom acc\n\n-(void)setTimeout:(NSTimeInterval)timeout {\n    @synchronized(self) {\n        if (self.isPinging) {\n            if (self.debug) {\n                NSLog(@\"GBPing: can't set timeout while pinger is running.\");\n            }\n        }\n        else {\n            _timeout = timeout;\n        }\n    }\n}\n\n-(NSTimeInterval)timeout {\n    @synchronized(self) {\n        if (!_timeout) {\n            return kDefaultTimeout;\n        }\n        else {\n            return _timeout;\n        }\n    }\n}\n\n-(void)setTtl:(NSUInteger)ttl {\n    @synchronized(self) {\n        if (self.isPinging) {\n            if (self.debug) {\n                NSLog(@\"GBPing: can't set ttl while pinger is running.\");\n            }\n        }\n        else {\n            _ttl = ttl;\n        }\n    }\n}\n\n-(NSUInteger)ttl {\n    @synchronized(self) {\n        if (!_ttl) {\n            return kDefaultTTL;\n        }\n        else {\n            return _ttl;\n        }\n    }\n}\n\n-(void)setCount:(NSUInteger)count {\n    @synchronized(self) {\n        if (self.isPinging) {\n            if (self.debug) {\n                NSLog(@\"GBPing: can't set count while pinger is running.\");\n            }\n        }\n        else {\n            _count = count;\n        }\n    }\n}\n\n-(NSUInteger)count {\n    @synchronized(self) {\n        if (!_count) {\n            return 0;\n        }\n        else {\n            return _count;\n        }\n    }\n}\n\n-(void)setPayloadSize:(NSUInteger)payloadSize {\n    @synchronized(self) {\n        if (self.isPinging) {\n            if (self.debug) {\n                NSLog(@\"GBPing: can't set payload size while pinger is running.\");\n            }\n        }\n        else {\n            _payloadSize = payloadSize;\n        }\n    }\n}\n\n-(NSUInteger)payloadSize {\n    @synchronized(self) {\n        if (!_payloadSize) {\n            return kDefaultPayloadSize;\n        }\n        else {\n            return _payloadSize;\n        }\n    }\n}\n\n-(void)setPingPeriod:(NSTimeInterval)pingPeriod {\n    @synchronized(self) {\n        if (self.isPinging) {\n            if (self.debug) {\n                NSLog(@\"GBPing: can't set pingPeriod while pinger is running.\");\n            }\n        }\n        else {\n            _pingPeriod = pingPeriod;\n        }\n    }\n}\n\n-(NSTimeInterval)pingPeriod {\n    @synchronized(self) {\n        if (!_pingPeriod) {\n            return (NSTimeInterval)kDefaultPingPeriod;\n        }\n        else {\n            return _pingPeriod;\n        }\n    }\n}\n\n#pragma mark - core pinging methods\n\n-(void)setupWithBlock:(StartupCallback)callback {\n    //error out of its already setup\n    if (self.isReady) {\n        if (self.debug) {\n            NSLog(@\"GBPing: Can't setup, already setup.\");\n        }\n        \n        //notify about error and return\n        dispatch_async(dispatch_get_main_queue(), ^{\n            callback(NO, nil);\n        });\n        return;\n    }\n    \n    //error out if no host is set\n    if (!self.host) {\n        if (self.debug) {\n            NSLog(@\"GBPing: set host before attempting to start.\");\n        }\n        \n        //notify about error and return\n        dispatch_async(dispatch_get_main_queue(), ^{\n            callback(NO, nil);\n        });\n        return;\n    }\n    \n    //set up data structs\n    self.nextSequenceNumber = 0;\n    @synchronized (self) {\n        self.pendingPings = [[NSMutableDictionary alloc] init];\n        self.timeoutTimers = [[NSMutableDictionary alloc] init];\n    }\n    \n    dispatch_async(self.setupQueue, ^{\n        CFStreamError streamError;\n        BOOL success;\n        \n        CFHostRef hostRef = CFHostCreateWithName(NULL, (__bridge CFStringRef)self.host);\n        \n        /*\n         * CFHostCreateWithName will return a null result in certain cases.\n         * CFHostStartInfoResolution will return YES if _hostRef is null.\n         */\n        if (hostRef) {\n            success = CFHostStartInfoResolution(hostRef, kCFHostAddresses, &streamError);\n        } else {\n            success = NO;\n        }\n        \n        if (!success) {\n            //construct an error\n            NSDictionary *userInfo;\n            NSError *error;\n            \n            if (hostRef && streamError.domain == kCFStreamErrorDomainNetDB) {\n                userInfo = [NSDictionary dictionaryWithObjectsAndKeys:\n                            [NSNumber numberWithInteger:streamError.error], kCFGetAddrInfoFailureKey,\n                            nil\n                            ];\n            }\n            else {\n                userInfo = nil;\n            }\n            error = [NSError errorWithDomain:(NSString *)kCFErrorDomainCFNetwork code:kCFHostErrorUnknown userInfo:userInfo];\n            \n            //clean up so far\n            [self stop];\n            \n            //notify about error and return\n            dispatch_async(dispatch_get_main_queue(), ^{\n                callback(NO, error);\n            });\n          \n            //just incase\n            if (hostRef) {\n              CFRelease(hostRef);\n            }\n            return;\n        }\n        \n        //get the first IPv4 or IPv6 address\n        Boolean resolved;\n        NSArray *addresses = (__bridge NSArray *)CFHostGetAddressing(hostRef, &resolved);\n        if (resolved && (addresses != nil)) {\n            resolved = false;\n            for (NSData *address in addresses) {\n                const struct sockaddr *anAddrPtr = (const struct sockaddr *)[address bytes];\n                \n                if ([address length] >= sizeof(struct sockaddr) &&\n                    ((self.useIpv4 && anAddrPtr->sa_family == AF_INET) ||\n                     (self.useIpv6 && anAddrPtr->sa_family == AF_INET6)) ) {\n                    resolved = true;\n                    self.hostAddress = address;\n                    self.hostAddressString = [self ntop:(struct sockaddr *)anAddrPtr len:(socklen_t)address.length];\n                    break;\n                }\n            }\n        }\n        \n        //we can stop host resolution now\n        if (hostRef) {\n            CFRelease(hostRef);\n        }\n        \n        //if an error occurred during resolution\n        if (!resolved) {\n            //stop\n            [self stop];\n            \n            //notify about error and return\n            dispatch_async(dispatch_get_main_queue(), ^{\n                callback(NO, [NSError errorWithDomain:(NSString *)kCFErrorDomainCFNetwork code:kCFHostErrorHostNotFound userInfo:nil]);\n            });\n            return;\n        }\n        \n        //set up socket\n        int err = 0;\n        switch (self.hostAddressFamily) {\n            case AF_INET: {\n                self.socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);\n                if (self.socket < 0) {\n                    err = errno;\n                }\n            } break;\n            case AF_INET6: {\n                self.socket = socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);\n                if (self.socket < 0) {\n                    err = errno;\n                }\n            } break;\n            default: {\n                err = EPROTONOSUPPORT;\n            } break;\n        }\n        \n        //couldnt setup socket\n        if (err) {\n            //clean up so far\n            [self stop];\n            \n            //notify about error and close\n            dispatch_async(dispatch_get_main_queue(), ^{\n                callback(NO, [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]);\n            });\n            return;\n        }\n        \n        //set ttl on the socket\n        if (self.ttl) {\n            u_char ttlForSockOpt = (u_char)self.ttl;\n            setsockopt(self.socket, IPPROTO_IP, IP_TTL, &ttlForSockOpt, sizeof(NSUInteger));\n        }\n        \n        //we are ready now\n        self.isReady = YES;\n        \n        //notify that we are ready\n        dispatch_async(dispatch_get_main_queue(), ^{\n            callback(YES, nil);\n        });\n    });\n    \n    self.isStopped = NO;\n}\n\n\n-(void)startPinging {\n    if (self.isReady && !self.isPinging) {\n        //go into infinite listenloop on a new thread (listenThread)\n        NSThread *listenThread = [[NSThread alloc] initWithTarget:self selector:@selector(listenLoop) object:nil];\n        listenThread.name = @\"listenThread\";\n        \n        //set up loop that sends packets on a new thread (sendThread)\n        NSThread *sendThread = [[NSThread alloc] initWithTarget:self selector:@selector(sendLoop) object:nil];\n        sendThread.name = @\"sendThread\";\n        \n        //we're pinging now\n        self.isPinging = YES;\n        [listenThread start];\n        [sendThread start];\n    }\n}\n\n-(void)listenLoop {\n    @autoreleasepool {\n        while (self.isPinging) {\n            [self listenOnce];\n        }\n    }\n}\n\n-(void)listenOnce {\n    int                     err;\n    struct sockaddr_storage addr;\n    socklen_t               addrLen;\n    ssize_t                 bytesRead;\n    void *                  buffer;\n    enum { kBufferSize = 65535 };\n    \n    buffer = malloc(kBufferSize);\n\n    if (buffer == nil) {\n        err = errno;\n        return;\n    }\n    \n    //read the data.\n    addrLen = sizeof(addr);\n    bytesRead = recvfrom(self.socket, buffer, kBufferSize, 0, (struct sockaddr *)&addr, &addrLen);\n    err = 0;\n    if (bytesRead < 0) {\n        err = errno;\n    }\n\n    //process the data we read.\n    if (bytesRead > 0) {\n        _endTime = CFAbsoluteTimeGetCurrent();\n        struct sockaddr_in *sin = (struct sockaddr_in *)&addr;\n        NSString *host = [self ntop:(struct sockaddr *)&addr len:addrLen];\n\n        if([host isEqualToString:self.hostAddressString]) { // only make sense where received packet comes from expected source\n            NSDate *receiveDate = [NSDate date];\n            NSMutableData *packet;\n\n            packet = [NSMutableData dataWithBytes:buffer length:(NSUInteger) bytesRead];\n\n            if (packet == nil) {\n                err = errno;\n                return;\n            }\n\n            //complete the ping summary\n            const struct ICMPHeader *headerPointer;\n            \n            if (sin->sin_family == AF_INET) {\n                headerPointer = [[self class] icmp4InPacket:packet];\n            } else {\n                headerPointer = (const struct ICMPHeader *)[packet bytes];\n            }\n            \n            NSUInteger seqNo = (NSUInteger)OSSwapBigToHostInt16(headerPointer->sequenceNumber);\n            NSNumber *key = @(seqNo);\n            GBPingSummary *pingSummary;\n            @synchronized (self) {\n                pingSummary = [(GBPingSummary *)self.pendingPings[key] copy];\n            }\n\n            if (pingSummary) {\n                if ([self isValidPingResponsePacket:packet]) {\n                    pingSummary.receiveDate = receiveDate;\n                    if (sin->sin_family == AF_INET) {\n                        //set ttl from response (different servers may respond with different ttls)\n                        const struct IPHeader *ipPtr;\n                        if ([packet length] >= sizeof(IPHeader)) {\n                            ipPtr = (const IPHeader *)[packet bytes];\n                             pingSummary.ttl = ipPtr->timeToLive;\n                        }\n                    }\n\n                    pingSummary.status = GBPingStatusSuccess;\n\n                    //invalidate the timeouttimer\n                    @synchronized (self) {\n                        NSTimer *timer = self.timeoutTimers[key];\n                        [timer invalidate];\n                        [self.timeoutTimers removeObjectForKey:key];\n                    }\n                    \n                    dispatch_async(dispatch_get_main_queue(), ^{\n                        //notify delegate\n                        if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didReceiveReplyWithSummary:)] ) {\n                            [self.delegate ping:self didReceiveReplyWithSummary:[pingSummary copy]];\n                        }\n                    });\n                }\n                else {\n                    pingSummary.status = GBPingStatusFail;\n                    \n                    dispatch_async(dispatch_get_main_queue(), ^{\n                        if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didReceiveUnexpectedReplyWithSummary:)] ) {\n                            [self.delegate ping:self didReceiveUnexpectedReplyWithSummary:[pingSummary copy]];\n                        }\n                    });\n                }\n            }\n        }\n    }\n    else {\n\n        //we failed to read the data, so shut everything down.\n        if (err == 0) {\n            err = EPIPE;\n        }\n        \n        @synchronized(self) {\n            if (!self.isStopped) {\n                dispatch_async(dispatch_get_main_queue(), ^{\n                    if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didFailWithError:)] ) {\n                        [self.delegate ping:self didFailWithError:[NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]];\n                    }\n                });\n            }\n        }\n        \n        //stop the whole thing\n        [self stop];\n    }\n    \n    free(buffer);\n}\n\n-(void)sendLoop {\n    @autoreleasepool {\n        self.counter = _count;\n        BOOL stopping = NO;\n        NSTimeInterval startTime = CFAbsoluteTimeGetCurrent();\n        _endTime = 0;\n        while (self.isPinging) {\n            [self sendPing];\n          \n            if (_count > 0) {\n                self.counter -= 1;\n                if (self.counter == 0) {\n                    stopping = YES;\n                }\n            }\n\n            NSTimeInterval runUntil = CFAbsoluteTimeGetCurrent() + (stopping ? self.timeout : self.pingPeriod);\n            NSTimeInterval time = 0;\n            while (runUntil > time) {\n                NSDate *runUntilDate = [NSDate dateWithTimeIntervalSinceReferenceDate:runUntil];\n                [[NSRunLoop currentRunLoop] runUntilDate:runUntilDate];\n        \n                time = CFAbsoluteTimeGetCurrent();\n            }\n            if (stopping) {\n              break;\n            }\n        }\n        [self stop];\n        dispatch_async(dispatch_get_main_queue(), ^{\n            if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didFinishWithTime:)] ) {\n                NSTimeInterval interval = 0;\n                if (self->_endTime > 0) {\n                    interval = self->_endTime - startTime;\n                }\n                [self.delegate ping:self didFinishWithTime:interval];\n            }\n        });\n    }\n}\n\n-(void)sendPing {\n    if (self.isPinging) {\n      \n        int err;\n        NSData *packet;\n        ssize_t bytesSent;\n        \n        // Construct the ping packet.\n        NSData *payload = [self generateDataWithLength:(self.payloadSize)];\n        \n        switch (self.hostAddressFamily) {\n            case AF_INET: {\n                packet = [self pingPacketWithType:kICMPv4TypeEchoRequest payload:payload requiresChecksum:YES];\n            } break;\n            case AF_INET6: {\n                packet = [self pingPacketWithType:kICMPv6TypeEchoRequest payload:payload requiresChecksum:NO];\n            } break;\n            default: {\n                err = errno;\n                return;\n            } break;\n        }\n        \n        // this is our ping summary\n        GBPingSummary *newPingSummary = [GBPingSummary new];\n        \n        // Send the packet.\n        if (self.socket == 0) {\n            bytesSent = -1;\n            err = EBADF;\n        }\n        else {\n            \n            //record the send date\n            NSDate *sendDate = [NSDate date];\n            \n            //construct ping summary, as much as it can\n            newPingSummary.sequenceNumber = self.nextSequenceNumber;\n            newPingSummary.host = self.host;\n            newPingSummary.ip = self.hostAddressString;\n            newPingSummary.sendDate = sendDate;\n            newPingSummary.ttl = self.ttl;\n            newPingSummary.payloadSize = self.payloadSize;\n            newPingSummary.status = GBPingStatusPending;\n            \n            //add it to pending pings\n            NSNumber *key = @(self.nextSequenceNumber);\n            @synchronized (self) {\n                self.pendingPings[key] = newPingSummary;\n            }\n            \n            //increment sequence number\n            self.nextSequenceNumber += 1;\n            \n            //we create a copy, this one will be passed out to other threads\n            GBPingSummary *pingSummaryCopy = [newPingSummary copy];\n            \n            //we need to clean up our list of pending pings, and we do that after the timeout has elapsed (+ some grace period)\n            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((self.timeout + kPendingPingsCleanupGrace) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{\n                //remove the ping from the pending list\n                @synchronized (self) {\n                    [self.pendingPings removeObjectForKey:key];\n                }\n            });\n            \n            //add a timeout timer\n            //add a timeout timer\n            NSTimer *timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:self.timeout\n                                                                     target:[NSBlockOperation blockOperationWithBlock:^{\n\n                                                                         newPingSummary.status = GBPingStatusFail;\n                                                                         self->_endTime = CFAbsoluteTimeGetCurrent();\n\n                                                                         //notify about the failure\n                                                                         dispatch_async(dispatch_get_main_queue(), ^{\n                                                                             if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didTimeoutWithSummary:)]) {\n                                                                                 [self.delegate ping:self didTimeoutWithSummary:pingSummaryCopy];\n                                                                             }\n                                                                         });\n\n                                                                         //remove the timer itself from the timers list\n                                                                         //lm make sure that the timer list doesnt grow and these removals actually work... try logging the count of the timeoutTimers when stopping the pinger\n                                                                         @synchronized (self) {\n                                                                             [self.timeoutTimers removeObjectForKey:key];\n                                                                         }\n                                                                     }]\n                                                                   selector:@selector(main)\n                                                                   userInfo:nil\n                                                                    repeats:NO];\n            [[NSRunLoop mainRunLoop] addTimer:timeoutTimer forMode:NSRunLoopCommonModes];\n            \n            //keep a local ref to it\n            if (self.timeoutTimers) {\n                @synchronized (self) {\n                    self.timeoutTimers[key] = timeoutTimer;\n                }\n            }\n            \n            //notify delegate about this\n            dispatch_async(dispatch_get_main_queue(), ^{\n                if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didSendPingWithSummary:)]) {\n                    [self.delegate ping:self didSendPingWithSummary:pingSummaryCopy];\n                }\n            });\n            \n            bytesSent = sendto(\n                               self.socket,\n                               [packet bytes],\n                               [packet length],\n                               0,\n                               (struct sockaddr *) [self.hostAddress bytes],\n                               (socklen_t) [self.hostAddress length]\n                               );\n            err = 0;\n            if (bytesSent < 0) {\n                err = errno;\n            }\n        }\n        \n        // This is after the sending\n        \n        //successfully sent\n        if ((bytesSent > 0) && (((NSUInteger) bytesSent) == [packet length])) {\n            //noop, we already notified delegate about sending of the ping\n        }\n        //failed to send\n        else {\n            //complete the error\n            if (err == 0) {\n                err = ENOBUFS;          // This is not a hugely descriptor error, alas.\n            }\n            \n            //little log\n            if (self.debug) {\n                NSLog(@\"GBPing: failed to send packet with error code: %d\", err);\n            }\n            \n            //change status\n            newPingSummary.status = GBPingStatusFail;\n            \n            GBPingSummary *pingSummaryCopyAfterFailure = [newPingSummary copy];\n            \n            //notify delegate\n            dispatch_async(dispatch_get_main_queue(), ^{\n                if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didFailToSendPingWithSummary:error:)]) {\n                    [self.delegate ping:self didFailToSendPingWithSummary:pingSummaryCopyAfterFailure error:[NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]];\n                }\n            });\n        }\n    }\n}\n\n-(void)stop {\n    @synchronized(self) {\n        if (!self.isStopped) {\n            self.isPinging = NO;\n            \n            self.isReady = NO;\n            \n            //destroy listenThread by closing socket (listenThread)\n            if (self.socket) {\n                close(self.socket);\n                self.socket = 0;\n            }\n            \n            //destroy host\n            self.hostAddress = nil;\n            \n            //clean up pendingpings\n            [self.pendingPings removeAllObjects];\n            self.pendingPings = nil;\n            for (NSNumber *key in [self.timeoutTimers copy]) {\n                NSTimer *timer = self.timeoutTimers[key];\n                [timer invalidate];\n            }\n            \n            //clean up timeouttimers\n            [self.timeoutTimers removeAllObjects];\n            self.timeoutTimers = nil;\n            \n            //reset seq number\n            self.nextSequenceNumber = 0;\n            \n            self.isStopped = YES;\n        }\n    }\n}\n\n#pragma mark - util\n\nstatic uint16_t in_cksum(const void *buffer, size_t bufferLen)\n// This is the standard BSD checksum code, modified to use modern types.\n{\n    size_t              bytesLeft;\n    int32_t             sum;\n    const uint16_t *    cursor;\n    union {\n        uint16_t        us;\n        uint8_t         uc[2];\n    } last;\n    uint16_t            answer;\n    \n    bytesLeft = bufferLen;\n    sum = 0;\n    cursor = buffer;\n    \n    /*\n     * Our algorithm is simple, using a 32 bit accumulator (sum), we add\n     * sequential 16 bit words to it, and at the end, fold back all the\n     * carry bits from the top 16 bits into the lower 16 bits.\n     */\n    while (bytesLeft > 1) {\n        sum += *cursor;\n        cursor += 1;\n        bytesLeft -= 2;\n    }\n    \n    /* mop up an odd byte, if necessary */\n    if (bytesLeft == 1) {\n        last.uc[0] = * (const uint8_t *) cursor;\n        last.uc[1] = 0;\n        sum += last.us;\n    }\n    \n    /* add back carry outs from top 16 bits to low 16 bits */\n    sum = (sum >> 16) + (sum & 0xffff);  /* add hi 16 to low 16 */\n    sum += (sum >> 16);      /* add carry */\n    answer = (uint16_t) ~sum;   /* truncate to 16 bits */\n    \n    return answer;\n}\n\n+(NSString *)sourceAddressInPacket:(NSData *)packet {\n    // Returns the source address of the IP packet\n    \n    const struct IPHeader   *ipPtr;\n    const uint8_t           *sourceAddress;\n    \n    if ([packet length] >= sizeof(IPHeader)) {\n        ipPtr = (const IPHeader *)[packet bytes];\n        \n        sourceAddress = ipPtr->sourceAddress;//dont need to swap byte order those cuz theyre the smallest atomic unit (1 byte)\n        NSString *ipString = [NSString stringWithFormat:@\"%d.%d.%d.%d\", sourceAddress[0], sourceAddress[1], sourceAddress[2], sourceAddress[3]];\n        \n        return ipString;\n    }\n    else return nil;\n}\n\n+ (NSUInteger)icmp4HeaderOffsetInPacket:(NSData *)packet\n// Returns the offset of the ICMPHeader within an IP packet.\n{\n    NSUInteger              result;\n    const struct IPHeader * ipPtr;\n    size_t                  ipHeaderLength;\n    \n    result = NSNotFound;\n    if ([packet length] >= (sizeof(IPHeader) + sizeof(ICMPHeader))) {\n        ipPtr = (const IPHeader *) [packet bytes];\n        assert((ipPtr->versionAndHeaderLength & 0xF0) == 0x40);     // IPv4\n        assert(ipPtr->protocol == 1);                               // ICMP\n        ipHeaderLength = (ipPtr->versionAndHeaderLength & 0x0F) * sizeof(uint32_t);\n        if ([packet length] >= (ipHeaderLength + sizeof(ICMPHeader))) {\n            result = ipHeaderLength;\n        }\n    }\n    return result;\n}\n\n+ (const struct ICMPHeader *)icmp4InPacket:(NSData *)packet\n// See comment in header.\n{\n    const struct ICMPHeader *   result;\n    NSUInteger                  icmpHeaderOffset;\n    \n    result = nil;\n    icmpHeaderOffset = [self icmp4HeaderOffsetInPacket:packet];\n    if (icmpHeaderOffset != NSNotFound) {\n        result = (const struct ICMPHeader *) (((const uint8_t *)[packet bytes]) + icmpHeaderOffset);\n    }\n    return result;\n}\n\n- (BOOL)isValidPingResponsePacket:(NSMutableData *)packet\n{\n    BOOL result;\n    \n    switch (self.hostAddressFamily) {\n        case AF_INET: {\n            result = [self isValidPing4ResponsePacket:packet];\n        } break;\n        case AF_INET6: {\n            result = [self isValidPing6ResponsePacket:packet];\n        } break;\n        default: {\n            result = NO;\n        } break;\n    }\n    return result;\n}\n\n- (BOOL)isValidPing4ResponsePacket:(NSMutableData *)packet\n// Returns true if the packet looks like a valid ping response packet destined\n// for us.\n{\n    BOOL                result;\n    NSUInteger          icmpHeaderOffset;\n    ICMPHeader *        icmpPtr;\n    uint16_t            receivedChecksum;\n    uint16_t            calculatedChecksum;\n    \n    result = NO;\n    \n    icmpHeaderOffset = [[self class] icmp4HeaderOffsetInPacket:packet];\n    if (icmpHeaderOffset != NSNotFound) {\n        icmpPtr = (struct ICMPHeader *) (((uint8_t *)[packet mutableBytes]) + icmpHeaderOffset);\n        \n        receivedChecksum   = icmpPtr->checksum;\n        icmpPtr->checksum  = 0;\n        calculatedChecksum = in_cksum(icmpPtr, [packet length] - icmpHeaderOffset);\n        icmpPtr->checksum  = receivedChecksum;\n        \n        if (receivedChecksum == calculatedChecksum) {\n            if ( (icmpPtr->type == kICMPv4TypeEchoReply) && (icmpPtr->code == 0) ) {\n                if ( OSSwapBigToHostInt16(icmpPtr->identifier) == self.identifier ) {\n                    if ( OSSwapBigToHostInt16(icmpPtr->sequenceNumber) < self.nextSequenceNumber ) {\n                        result = YES;\n                    }\n                }\n            }\n        }\n    }\n    \n    //    NSLog(@\"valid: %@, type: %d\", _b(result), icmpPtr->type);\n    \n    return result;\n}\n\n- (BOOL)isValidPing6ResponsePacket:(NSMutableData *)packet\n// Returns true if the IPv6 packet looks like a valid ping response packet destined\n// for us.\n{\n    BOOL                      result;\n    const ICMPHeader *        icmpPtr;\n    \n    result = NO;\n    \n    if (packet.length >= sizeof(*icmpPtr)) {\n        icmpPtr = packet.bytes;\n        \n        if ( (icmpPtr->type == kICMPv6TypeEchoReply) && (icmpPtr->code == 0) ) {\n            if ( OSSwapBigToHostInt16(icmpPtr->identifier) == self.identifier ) {\n                if ( OSSwapBigToHostInt16(icmpPtr->sequenceNumber) < self.nextSequenceNumber ) {\n                    result = YES;\n                }\n            }\n        }\n\n    }\n    \n    return result;\n}\n\n-(NSData *)generateDataWithLength:(NSUInteger)length {\n    //create a buffer full of 7's of specified length\n    char tempBuffer[length];\n    memset(tempBuffer, 7, length);\n    \n    return [[NSData alloc] initWithBytes:tempBuffer length:length];\n}\n\n- (void)_invokeTimeoutCallback:(NSTimer *)timer\n{\n    dispatch_block_t callback = timer.userInfo;\n    if (callback) {\n        callback();\n    }\n}\n\n- (NSData *)pingPacketWithType:(uint8_t)type payload:(NSData *)payload requiresChecksum:(BOOL)requiresChecksum {\n    NSMutableData *         packet;\n    ICMPHeader *            icmpPtr;\n    \n    packet = [NSMutableData dataWithLength:sizeof(*icmpPtr) + payload.length];\n    if (packet == nil) { return nil; }\n    \n    icmpPtr = packet.mutableBytes;\n    icmpPtr->type = type;\n    icmpPtr->code = 0;\n    icmpPtr->checksum = 0;\n    icmpPtr->identifier     = OSSwapHostToBigInt16(self.identifier);\n    icmpPtr->sequenceNumber = OSSwapHostToBigInt16(self.nextSequenceNumber);\n    memcpy(&icmpPtr[1], [payload bytes], [payload length]);\n    \n    if (requiresChecksum) {\n        // The IP checksum routine returns a 16-bit number that's already in correct byte order\n        // (due to wacky 1's complement maths), so we just put it into the packet as a 16-bit unit.\n        \n        icmpPtr->checksum = in_cksum(packet.bytes, packet.length);\n    }\n    \n    return packet;\n}\n\n- (sa_family_t)hostAddressFamily {\n    sa_family_t result = AF_UNSPEC;\n    // Save a reference to a local variable, avoid crash when hostAddress is release by other thread.\n    NSData *hostAddress = self.hostAddress; \n    if (hostAddress != nil && hostAddress.length >= sizeof(struct sockaddr)) {\n        result = ((const struct sockaddr *)hostAddress.bytes)->sa_family;\n    }\n    \n    return result;\n}\n\n- (NSString*)ntop:(struct sockaddr *)sa len:(socklen_t)len {\n    char ntop[NI_MAXHOST] = { 0 };\n    int ecode = getnameinfo(sa, len, ntop, sizeof(ntop), NULL, 0, NI_NUMERICHOST);\n    if (ecode == 0) {\n        return [[NSString alloc] initWithUTF8String:ntop];\n    } else {\n        return nil;\n    }\n}\n\n#pragma mark - memory\n\n-(id)init {\n    if (self = [super init]) {\n        self.setupQueue = dispatch_queue_create(\"GBPing setup queue\", 0);\n        self.isStopped = YES;\n        self.identifier = arc4random();\n        self.useIpv4 = YES;\n        self.useIpv6 = YES;\n    }\n    \n    return self;\n}\n\n-(void)dealloc {\n    self.delegate = nil;\n    self.host = nil;\n    @synchronized (self) {\n        self.timeoutTimers = nil;\n        self.pendingPings = nil;\n    }\n    self.hostAddress = nil;\n    \n    //clean up socket to be sure\n    if (self.socket) {\n        close(self.socket);\n        self.socket = 0;\n    }\n}\n\n@end\n"
  },
  {
    "path": "ios/ProxyPin/vpn/ping/GBPingHelper.swift",
    "content": "//\n//  GBPingHelper.swift\n//\n\nimport Foundation\n\npublic typealias Handler = ((_ response: [String: Any]) -> Void)\n\npublic class GBPingHelper: NSObject {\n  private var ping: GBPing?\n  private let delegate = PingDelegate()\n\n  func start(withHost host: String, ipv4: Bool, ipv6: Bool, count: UInt, interval: TimeInterval, timeout: TimeInterval, ttl: UInt, handler: @escaping Handler) {\n    ping?.stop()\n    ping = GBPing()\n    guard let ping = ping else {\n      return\n    }\n    ping.host = host\n    ping.useIpv4 = ipv4\n    ping.useIpv6 = ipv6\n    ping.count = count\n    ping.pingPeriod = interval\n    ping.timeout = timeout\n    if ttl > 0 {\n      ping.ttl = ttl\n    }\n\n    delegate.handler = handler\n    ping.delegate = delegate\n    ping.setup { success, err in\n      if let err = err as NSError? {\n        if err.domain == kCFErrorDomainCFNetwork as String {\n          handler([\"error\": \"UnknownHost\"])\n        } else {\n          handler([\"error\": \"UnknownError\"])\n        }\n        return\n      }\n      if success {\n        self.delegate.transmitted = 0\n        self.delegate.received = 0\n        ping.startPinging()\n      }\n    }\n  }\n\n  func stop() {\n    ping?.stop()\n  }\n}\n\nprivate class PingDelegate: NSObject, GBPingDelegate {\n  public var handler: Handler?\n  public var transmitted = 0\n  public var received = 0\n\n  func handle(_ summary: GBPingSummary, error: String? = nil) {\n    guard let handler = handler else {\n      return\n    }\n    var ret: [String: Any] = [:]\n    ret[\"seq\"] = summary.sequenceNumber\n    ret[\"host\"] = summary.host\n    ret[\"ip\"] = summary.ip\n    ret[\"ttl\"] = summary.ttl\n    ret[\"time\"] = summary.rtt\n    ret[\"error\"] = error\n    handler(ret)\n  }\n\n  func ping(_ pinger: GBPing, didSendPingWith summary: GBPingSummary) {\n    transmitted += 1\n  }\n\n  func ping(_ pinger: GBPing, didTimeoutWith summary: GBPingSummary) {\n    handle(summary, error: \"RequestTimedOut\")\n  }\n\n  func ping(_ pinger: GBPing, didReceiveReplyWith summary: GBPingSummary) {\n    received += 1\n    handle(summary)\n  }\n\n  func ping(_ pinger: GBPing, didFinishWithTime time: TimeInterval) {\n    guard let handler = handler else {\n      return\n    }\n    var ret: [String: Any] = [:]\n    ret[\"time\"] = time\n    ret[\"received\"] = received\n    ret[\"transmitted\"] = transmitted\n    handler(ret)\n  }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/ping/GBPingSummary.h",
    "content": "//\n//  GBPingSummary.h\n//  GBPing\n//\n//  Created by Luka Mirosevic on 05/11/2012.\n//  Copyright (c) 2012 Goonbee. All rights reserved.\n//\n\n#import <Foundation/Foundation.h>\n\n@interface GBPingSummary : NSObject <NSCopying>\n\ntypedef enum {\n    GBPingStatusPending,\n    GBPingStatusSuccess,\n    GBPingStatusFail,\n} GBPingStatus;\n\n@property (assign, nonatomic) NSUInteger        sequenceNumber;\n@property (assign, nonatomic) NSUInteger        payloadSize;\n@property (assign, nonatomic) NSUInteger        ttl;\n@property (strong, nonatomic, nullable) NSString          *host;\n@property (strong, nonatomic, nullable) NSString          *ip;\n@property (strong, nonatomic, nullable) NSDate            *sendDate;\n@property (strong, nonatomic, nullable) NSDate            *receiveDate;\n@property (assign, nonatomic) NSTimeInterval    rtt;\n@property (assign, nonatomic) GBPingStatus      status;\n\n@end\n"
  },
  {
    "path": "ios/ProxyPin/vpn/ping/GBPingSummary.m",
    "content": "//\n//  GBPingSummary.m\n//  GBPing\n//\n//  Created by Luka Mirosevic on 05/11/2012.\n//  Copyright (c) 2012 Goonbee. All rights reserved.\n//\n\n#import \"GBPingSummary.h\"\n\n@implementation GBPingSummary\n\n#pragma mark - custom acc\n\n-(void)setHost:(NSString *)host {\n    _host = host;\n}\n\n-(NSTimeInterval)rtt {\n    if (self.sendDate) {\n        return [self.receiveDate timeIntervalSinceDate:self.sendDate];\n    }\n    else {\n        return 0;\n    }\n}\n\n#pragma mark - copying\n\n-(id)copyWithZone:(NSZone *)zone {\n    GBPingSummary *copy = [[[self class] allocWithZone:zone] init];\n    \n    copy.sequenceNumber = self.sequenceNumber;\n    copy.payloadSize = self.payloadSize;\n    copy.ttl = self.ttl;\n    copy.host = [self.host copy];\n    copy.ip = [self.ip copy];\n    copy.sendDate = [self.sendDate copy];\n    copy.receiveDate = [self.receiveDate copy];\n    copy.status = self.status;\n    \n    return copy;\n}\n\n#pragma mark - memory\n\n-(id)init {\n    if (self = [super init]) {\n        self.status = GBPingStatusPending;\n    }\n    \n    return self;\n}\n\n-(void)dealloc {\n    self.host = nil;\n    self.ip = nil;\n    self.sendDate = nil;\n    self.receiveDate = nil;\n}\n\n#pragma mark - description\n\n-(NSString *)description {\n    return [NSString stringWithFormat:@\"host: %@, ip:%@, seq: %lu, status: %d, ttl: %lu, payloadSize: %lu, sendDate: %@, receiveDate: %@, rtt: %f\", self.host, self.ip, (unsigned long)self.sequenceNumber, self.status, (unsigned long)self.ttl, (unsigned long)self.payloadSize, self.sendDate, self.receiveDate, self.rtt];\n}\n\n@end\n"
  },
  {
    "path": "ios/ProxyPin/vpn/ping/ICMPHeader.h",
    "content": "//\n//  ICMPHeader.h\n//  GBPing\n//\n//  Created by Luka Mirosevic on 15/11/2012.\n//  Copyright (c) 2012 Goonbee. All rights reserved.\n//\n\n#ifndef GBPing_ICMPHeader_h\n#define GBPing_ICMPHeader_h\n\n#include <AssertMacros.h>\n\n#pragma mark - IP and ICMP On-The-Wire Format\n\n// The following declarations specify the structure of ping packets on the wire.\n\n// IP header structure:\n\nstruct IPHeader {\n    uint8_t     versionAndHeaderLength;\n    uint8_t     differentiatedServices;\n    uint16_t    totalLength;\n    uint16_t    identification;\n    uint16_t    flagsAndFragmentOffset;\n    uint8_t     timeToLive;\n    uint8_t     protocol;\n    uint16_t    headerChecksum;\n    uint8_t     sourceAddress[4];\n    uint8_t     destinationAddress[4];\n    // options...\n    // data...\n};\ntypedef struct IPHeader IPHeader;\n\n__Check_Compile_Time(sizeof(IPHeader) == 20);\n__Check_Compile_Time(offsetof(IPHeader, versionAndHeaderLength) == 0);\n__Check_Compile_Time(offsetof(IPHeader, differentiatedServices) == 1);\n__Check_Compile_Time(offsetof(IPHeader, totalLength) == 2);\n__Check_Compile_Time(offsetof(IPHeader, identification) == 4);\n__Check_Compile_Time(offsetof(IPHeader, flagsAndFragmentOffset) == 6);\n__Check_Compile_Time(offsetof(IPHeader, timeToLive) == 8);\n__Check_Compile_Time(offsetof(IPHeader, protocol) == 9);\n__Check_Compile_Time(offsetof(IPHeader, headerChecksum) == 10);\n__Check_Compile_Time(offsetof(IPHeader, sourceAddress) == 12);\n__Check_Compile_Time(offsetof(IPHeader, destinationAddress) == 16);\n\n// ICMP type and code combinations:\n\nenum {\n    kICMPv4TypeEchoRequest = 8,\n    kICMPv4TypeEchoReply   = 0\n};\n\nenum {\n    kICMPv6TypeEchoRequest = 128,\n    kICMPv6TypeEchoReply   = 129\n};\n\n// ICMP header structure:\n\nstruct ICMPHeader {\n    uint8_t     type;\n    uint8_t     code;\n    uint16_t    checksum;\n    uint16_t    identifier;\n    uint16_t    sequenceNumber;\n    // data...\n};\ntypedef struct ICMPHeader ICMPHeader;\n\n__Check_Compile_Time(sizeof(ICMPHeader) == 8);\n__Check_Compile_Time(offsetof(ICMPHeader, type) == 0);\n__Check_Compile_Time(offsetof(ICMPHeader, code) == 1);\n__Check_Compile_Time(offsetof(ICMPHeader, checksum) == 2);\n__Check_Compile_Time(offsetof(ICMPHeader, identifier) == 4);\n__Check_Compile_Time(offsetof(ICMPHeader, sequenceNumber) == 6);\n\n\n#endif\n"
  },
  {
    "path": "ios/ProxyPin/vpn/socket/ClientPacketWriter.swift",
    "content": "//\n//  ClientPacketWriter.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/\n\nimport Foundation\nimport NetworkExtension\n\nclass ClientPacketWriter: NSObject {\n    private var packetFlow: NEPacketTunnelFlow\n    private var isShutdown = false\n\n    init(packetFlow: NEPacketTunnelFlow) {\n        self.packetFlow = packetFlow\n    }\n\n    func write(data: Data) {\n        self.packetFlow.writePackets([data], withProtocols: [NSNumber(value: AF_INET)])\n    }\n\n    func shutdown() {\n        self.isShutdown = true\n    }\n}\n\n"
  },
  {
    "path": "ios/ProxyPin/vpn/socket/CloseableConnection.swift",
    "content": "//\n//  CloseableConnection.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/17.\n//\n\nimport Foundation\n\n\nprotocol CloseableConnection {\n    /// Closes the connection\n    func closeConnection(connection: Connection)\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/socket/SocketIOService.swift",
    "content": "//\n//  ProxySocketIOService.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/17.\n//\n\nimport Foundation\nimport NetworkExtension\nimport os.log\n\nclass SocketIOService {\n//    private static let maxReceiveBufferSize = 16384\n    private static let maxReceiveBufferSize = 1480\n\n    private let queue: DispatchQueue = DispatchQueue(label: \"ProxyPin.SocketIOService\", attributes: .concurrent)\n\n    private var clientPacketWriter: NEPacketTunnelFlow\n\n    private var shutdown = false\n\n    init(clientPacketWriter: NEPacketTunnelFlow) {\n        self.clientPacketWriter = clientPacketWriter\n    }\n\n    public func stop() {\n        os_log(\"Stopping SocketIOService\", log: OSLog.default, type: .default)\n        queue.async(flags: .barrier) {\n            self.shutdown = true\n        }\n//        queue.suspend()\n    }\n\n    //从connection接受数据 写到client\n    public func registerSession(connection: Connection) {\n        \n        connection.channel!.stateUpdateHandler = { state in\n//             os_log(\"Connection %{public}@ state changed to %{public}@\", log: OSLog.default, type: .default, connection.description, String(describing: state))\n            switch state {\n\n            case .ready:\n                connection.isConnected = true\n                os_log(\"Connected to %{public}@ on receiveMessage\", log: OSLog.default, type: .default, connection.description)\n\n                //接受远程服务器的数据\n                connection.sendToDestination()\n                self.receiveMessage(connection: connection)\n            case .cancelled:\n                connection.isConnected = false\n                os_log(\"Connection cancelled  %{public}@\", log: OSLog.default, type: .default, connection.description)\n                connection.closeConnection()\n                self.sendFin(connection: connection)\n\n            case .failed(let error):\n                connection.isConnected = false\n                os_log(\"Failed to connect: %{public}@ %{public}@\", log: OSLog.default, type: .error,connection.description, error.localizedDescription)\n                connection.closeConnection()\n            default:\n                os_log(\"Connection %{public}@ entered unhandled state: %{public}@\", log: OSLog.default, type: .default, connection.description, String(describing: state))\n                break\n            }\n        }\n\n        connection.channel!.start(queue: self.queue)\n    }\n\n    private func receiveMessage(connection: Connection) {\n        if (shutdown) {\n            os_log(\"SocketIOService is shutting down\", log: OSLog.default, type: .default)\n            return\n        }\n\n        if (connection.nwProtocol == .UDP) {\n            readUDP(connection: connection)\n        } else {\n            readTCP(connection: connection)\n        }\n\n        if (connection.isAbortingConnection) {\n            os_log(\"Connection is aborting\", log: OSLog.default, type: .default)\n            connection.closeConnection()\n            return\n        }\n    }\n\n    func readTCP(connection: Connection) {\n//         os_log(\"Reading from TCP socket\")\n        if connection.isAbortingConnection {\n            os_log(\"Connection is aborting\", log: OSLog.default, type: .default)\n            return\n        }\n\n        guard let channel = connection.channel else {\n            os_log(\"Invalid channel type\", log: OSLog.default, type: .error)\n            return\n        }\n        \n        channel.receive(minimumIncompleteLength: 1, maximumLength: Self.maxReceiveBufferSize) { (data, context, isComplete, error) in\n            self.queue.async(flags: .barrier) {\n//                 os_log(\"[SocketIOService] Received TCP data packet %{public}@ length %d\", log: OSLog.default, type: .default, connection.description, data?.count ?? -1)\n                if let error = error {\n                    os_log(\"Failed to read from TCP socket: %@\", log: OSLog.default, type: .error, error as CVarArg)\n                    connection.isAbortingConnection = true\n                    return\n                }\n\n                self.pushDataToClient(buffer: data ?? Data() , connection: connection)\n\n                // Recursively call readTCP to continue reading messages\n                self.receiveMessage(connection: connection)\n                \n                if (isComplete) {\n                    connection.isAbortingConnection = true\n                    return\n                }\n            }\n        }\n    }\n    \n    func synchronized(_ lock: AnyObject, closure: () -> Void) {\n//        objc_sync_enter(lock)\n        closure()\n//        objc_sync_exit(lock)\n    }\n    \n    ///create packet data and send it to VPN client\n    private func pushDataToClient(buffer: Data, connection: Connection) {\n        // Last piece of data is usually smaller than MAX_RECEIVE_BUFFER_SIZE. We use this as a\n        // trigger to set PSH on the resulting TCP packet that goes to the VPN.\n\n        connection.hasReceivedLastSegment = buffer.count <= 0\n\n        guard let ipHeader = connection.lastIpHeader, let tcpHeader = connection.lastTcpHeader else {\n            os_log(\"Invalid ipHeader or tcpHeader\", log: OSLog.default, type: .error)\n            return\n        }\n\n        synchronized(connection) {\n            let unAck = connection.sendNext\n            //处理益处问题\n            let nextUnAck = UInt32(truncatingIfNeeded: (connection.sendNext + UInt32(buffer.count)) % UInt32.max)\n            connection.sendNext = nextUnAck\n\n            let data = TCPPacketFactory.createResponsePacketData(\n                ipHeader: ipHeader,\n                tcpHeader: tcpHeader,\n                packetData: buffer,\n                isPsh: connection.hasReceivedLastSegment,\n                ackNumber: connection.recSequence,\n                seqNumber: unAck,\n                timeSender: connection.timestampSender,\n                timeReplyTo: connection.timestampReplyTo\n            )\n\n            self.clientPacketWriter.writePackets([data], withProtocols: [NSNumber(value: AF_INET)])\n//              os_log(\"[SocketIOService] Sent TCP data packet to client %{public}@ length:%d  seq:%u ack:%u\", log: OSLog.default, type: .default, connection.description, buffer.count, unAck, connection.recSequence)\n        }\n    }\n\n    private func sendFin(connection: Connection) {\n        if (connection.nwProtocol != .TCP) {\n            return\n        }\n        \n        guard let ipHeader = connection.lastIpHeader, let tcpHeader = connection.lastTcpHeader else {\n            os_log(\"Invalid ipHeader or tcpHeader\", log: OSLog.default, type: .error)\n            return\n        }\n        synchronized(connection) {\n            let data = TCPPacketFactory.createFinData(\n                ipHeader: ipHeader,\n                tcpHeader: tcpHeader,\n                ackNumber: connection.recSequence,\n                seqNumber: connection.sendNext,\n                timeSender: connection.timestampSender,\n                timeReplyTo: connection.timestampReplyTo\n            )\n            \n            self.clientPacketWriter.writePackets([data], withProtocols: [NSNumber(value: AF_INET)])\n        }\n    }\n    \n    func readUDP(connection: Connection) {\n \n        guard let channel = connection.channel else {\n            os_log(\"Invalid channel type\", log: OSLog.default, type: .error)\n            return\n        }\n\n        channel.receive(minimumIncompleteLength: 1, maximumLength: 65507) { (data, context, isComplete, error) in\n                self.queue.async(flags: .barrier) {\n                if let error = error {\n                    os_log(\"Failed to read from UDP socket: %@\", log: OSLog.default, type: .error, error as CVarArg)\n                    connection.isAbortingConnection = true\n                    return\n                }\n\n//                os_log(\"Received UDP data packet length %d\", log: OSLog.default, type: .debug, data?.count ?? 0)\n\n                guard let data = data, !data.isEmpty else {\n                    return\n                }\n                \n                guard let ipHeader = connection.lastIpHeader, let udpHeader = connection.lastUdpHeader else {\n                    os_log(\"Missing IP or UDP header for connection %{public}@\", log: OSLog.default, type: .error, connection.description)\n                    return\n                }\n                \n                let packetData = UDPPacketFactory.createResponsePacket(\n                    ip: ipHeader,\n                    udp: udpHeader,\n                    packetData: data\n                )\n\n                self.clientPacketWriter.writePackets([packetData], withProtocols: [NSNumber(value: AF_INET)])\n\n                // Recursively call receiveMessage to continue receiving messages\n                self.receiveMessage(connection: connection)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/transport/Packet.swift",
    "content": "//\n//  Packet.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/17.\n//\n\nimport Foundation\n\n\nclass Packet {\n    var ipHeader: IP4Header\n    var transportHeader: TransportHeader\n    var buffer: Data\n\n    init(ipHeader: IP4Header, transportHeader: TransportHeader, buffer: Data) {\n        self.ipHeader = ipHeader\n        self.transportHeader = transportHeader\n        self.buffer = buffer\n    }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/transport/protocol/ICMPPacket.swift",
    "content": "//\n//  ICMPPacket.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/10/3.\n//\n\nimport Foundation\n\nclass ICMPPacket {\n    // Two ICMP packets we can handle: simple ping & pong\n    static let ECHO_REQUEST_TYPE: UInt8 = 8\n    static let ECHO_SUCCESS_TYPE: UInt8 = 0\n\n    // One very common packet we ignore: connection rejection. Unclear why this happens,\n    // random incoming connections that the phone tries to reply to? Nothing we can do though,\n    // as we can't forward ICMP onwards, and we can't usefully respond or react.\n    static let DESTINATION_UNREACHABLE_TYPE: UInt8 = 3\n\n    let type: UInt8\n    let code: UInt8 // 0 for request, 0 for success, 0 - 15 for error subtypes\n    let checksum: UInt16\n    let identifier: UInt16\n    let sequenceNumber: UInt16\n    let data: [UInt8]\n\n    init(type: UInt8, code: UInt8, checksum: UInt16, identifier: UInt16, sequenceNumber: UInt16, data: [UInt8]) {\n        self.type = type\n        self.code = code\n        self.checksum = checksum\n        self.identifier = identifier\n        self.sequenceNumber = sequenceNumber\n        self.data = data\n    }\n\n    var description: String {\n        return \"ICMP packet type \\(type)/\\(code) id:\\(identifier) seq:\\(sequenceNumber) and \\(data.count) bytes of data\"\n    }\n}\n\n\nclass ICMPPacketFactory {\n    \n    static func parseICMPPacket(_ stream: inout Data) -> ICMPPacket? {\n        guard stream.count >= 8 else { return nil }\n        \n        let type = stream.removeFirst()\n        let code = stream.removeFirst()\n        let checksum = stream.withUnsafeBytes { $0.load(as: UInt16.self) }\n        stream.removeFirst(2)\n        \n        let identifier = stream.withUnsafeBytes { $0.load(as: UInt16.self) }\n        stream.removeFirst(2)\n        let sequenceNumber = stream.withUnsafeBytes { $0.load(as: UInt16.self) }\n        stream.removeFirst(2)\n        \n        let data = Array(stream)\n        \n        return ICMPPacket(type: type, code: code, checksum: checksum, identifier: identifier, sequenceNumber: sequenceNumber, data: data)\n    }\n    \n    static func buildSuccessPacket(_ requestPacket: ICMPPacket) -> ICMPPacket {\n        return ICMPPacket(\n            type: ICMPPacket.ECHO_SUCCESS_TYPE,\n            code: 0,\n            checksum: 0,\n            identifier: requestPacket.identifier,\n            sequenceNumber: requestPacket.sequenceNumber,\n            data: requestPacket.data\n        )\n    }\n    \n    static func packetToBuffer(ipHeader: IP4Header, packet: ICMPPacket) -> Data {\n        var buffer = Data()\n        buffer.append(ipHeader.toBytes())\n\n        var icmpDataBuffer = Data()\n        icmpDataBuffer.append(packet.type)\n        icmpDataBuffer.append(packet.code)\n        icmpDataBuffer.append(contentsOf: withUnsafeBytes(of: UInt16(0), Array.init))\n        \n        if packet.type == ICMPPacket.ECHO_REQUEST_TYPE || packet.type == ICMPPacket.ECHO_SUCCESS_TYPE {\n            icmpDataBuffer.append(contentsOf: packet.identifier.bytes)\n            icmpDataBuffer.append(contentsOf: packet.sequenceNumber.bytes)\n            icmpDataBuffer.append(contentsOf: packet.data)\n        } else {\n            fatalError(\"Can't serialize unrecognized ICMP packet type\")\n        }\n        \n        let checksum = PacketUtil.calculateChecksum(data: icmpDataBuffer, offset: 0, length: icmpDataBuffer.count)\n        icmpDataBuffer.replaceSubrange(2..<4, with: checksum)\n        buffer.append(icmpDataBuffer)\n\n        return buffer\n    }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/transport/protocol/IP4Header.swift",
    "content": "//\n//  IP4Header.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/16.\n//\n\nimport Foundation\nimport os.log\n\n// IPv4 header data structure\nclass IP4Header {\n    var ipVersion: UInt8 // 对于IPv4，其值为4（因此命名为IPv4）。 4bit\n    var internetHeaderLength: UInt8 // 头部长度 4bit\n    var diffTypeOfService: UInt8 // 差分服务代码点 =>6位\n    var ecn: UInt8 // 显式拥塞通知（ECN）\n    var totalLength: UInt16 // 此IP数据包的总长度 16bit\n    var identification: UInt16 // 主要用于唯一标识单个IP数据报的片段组。 16bit\n    var mayFragment: Bool // 用于指示数据报是否可以分段。 1bit\n    var lastFragment: Bool // 用于指示数据报是否是片段中的最后一个。 1bit\n    var fragmentOffset: UInt16 // 指定特定片段相对于原始未分段的IP数据报的开始的偏移量。 13bit\n    var timeToLive: UInt8 // 用于防止数据报持续存在。8bit\n    var protocolNumber: UInt8 // 定义IP数据报的数据部分中使用的协议。 8bit\n    var headerChecksum: UInt16 // 用于对头部进行错误检查的16位字段。 16bit\n    var sourceIP: UInt32 // 发送者的IPv4地址。 32bit\n    var destinationIP: UInt32 // 接收者的IPv4地址。 32bit\n\n    //用于控制或识别片段的3比特字段。\n    //bit 0: 保留；必须为零\n    //bit 1: Don't Fragment (DF)\n    //bit 2: More Fragments (MF)\n    private var flag: UInt8\n\n    init(\n        ipVersion: UInt8, internetHeaderLength: UInt8, diffTypeOfService: UInt8, ecn: UInt8, totalLength: UInt16, identification: UInt16,\n        mayFragment: Bool, lastFragment: Bool, fragmentOffset: UInt16, timeToLive: UInt8, protocolNumber: UInt8, headerChecksum: UInt16,\n        sourceIP: UInt32, destinationIP: UInt32\n    ) {\n        self.ipVersion = ipVersion\n        self.internetHeaderLength = internetHeaderLength\n        self.diffTypeOfService = diffTypeOfService\n        self.ecn = ecn\n        self.totalLength = totalLength\n        self.identification = identification\n        self.mayFragment = mayFragment\n        self.lastFragment = lastFragment\n        self.fragmentOffset = fragmentOffset\n        self.timeToLive = timeToLive\n        self.protocolNumber = protocolNumber\n        self.headerChecksum = headerChecksum\n        self.sourceIP = sourceIP\n        self.destinationIP = destinationIP\n        self.flag = IP4Header.initFlag(mayFragment: mayFragment, lastFragment: lastFragment)\n    }\n\n\n    private static func initFlag(mayFragment: Bool, lastFragment: Bool) -> UInt8 {\n        var initFlag: UInt8 = 0\n        if mayFragment {\n          initFlag = 0x40\n        }\n        if lastFragment {\n          initFlag |= 0x20\n        }\n        return initFlag\n    }\n\n    func setMayFragment(_ mayFragment: Bool) {\n        self.mayFragment = mayFragment\n        flag = mayFragment ? (flag | 0x40) : (flag & 0xBF)\n    }\n\n    func getIPHeaderLength() -> Int {\n        return Int(internetHeaderLength * 4)\n    }\n\n    func copy() -> IP4Header {\n       return IP4Header(\n           ipVersion: ipVersion, internetHeaderLength: internetHeaderLength, diffTypeOfService: diffTypeOfService, ecn: ecn, totalLength: totalLength, identification: identification,\n           mayFragment: mayFragment, lastFragment: lastFragment, fragmentOffset: fragmentOffset, timeToLive: timeToLive, protocolNumber: protocolNumber, headerChecksum: headerChecksum,\n           sourceIP: sourceIP, destinationIP: destinationIP\n       )\n    }\n\n    func toBytes() -> Data {\n        var buffer = Data()\n        buffer.append(UInt8((ipVersion << 4) + internetHeaderLength))\n        buffer.append(UInt8((diffTypeOfService << 2) + ecn))\n\n        buffer.append(contentsOf: totalLength.bytes)\n        buffer.append(contentsOf: identification.bytes)\n\n        //组合标志和部分片段偏移\n        buffer.append(UInt8((fragmentOffset >> 8) & 0x1F) | flag)\n        buffer.append(UInt8(fragmentOffset & 0xFF))\n\n        buffer.append(timeToLive)\n        buffer.append(protocolNumber)\n\n        buffer.append(contentsOf: headerChecksum.bytes)\n\n        buffer.append(contentsOf: sourceIP.bytes)\n        buffer.append(contentsOf: destinationIP.bytes)\n        return buffer\n    }\n}\n\nclass IPPacketFactory {\n   static let IP4_HEADER_SIZE = 20\n   static let IP4_VERSION: UInt8 = 0x04\n\n   //从给定的ByteBuffer流创建IPv4标头\n   static func createIP4Header(data: Data) -> IP4Header? {\n       guard data.count >= IP4_HEADER_SIZE else {\n           return nil\n       }\n\n       let buffer = [UInt8](data)\n       let versionAndHeaderLength = buffer[0]\n       let ipVersion = versionAndHeaderLength >> 4\n       guard ipVersion == IP4_VERSION else {\n           return nil\n       }\n\n       let internetHeaderLength = versionAndHeaderLength & 0x0F\n       let typeOfService = buffer[1]\n       let diffTypeOfService = typeOfService >> 2\n       let ecn = typeOfService & 0x03\n       let totalLength = UInt16(buffer[2]) << 8 | UInt16(buffer[3])\n       let identification = UInt16(buffer[4]) << 8 | UInt16(buffer[5])\n       let flagsAndFragmentOffset = UInt16(buffer[6]) << 8 | UInt16(buffer[7])\n       let mayFragment = (flagsAndFragmentOffset & 0x4000) != 0\n       let lastFragment = (flagsAndFragmentOffset & 0x2000) != 0\n       let fragmentOffset = flagsAndFragmentOffset & 0x1FFF\n       let timeToLive = buffer[8]\n       let protocolNumber = buffer[9]\n       let checksum = UInt16(buffer[10]) << 8 | UInt16(buffer[11])\n       let sourceIp = UInt32(buffer[12]) << 24 | UInt32(buffer[13]) << 16 | UInt32(buffer[14]) << 8 | UInt32(buffer[15])\n       let desIp = UInt32(buffer[16]) << 24 | UInt32(buffer[17]) << 16 | UInt32(buffer[18]) << 8 | UInt32(buffer[19])\n\n       if internetHeaderLength > 5 {\n           // drop the IP option\n           for _ in 0..<(internetHeaderLength - 5) {\n               // Skip the IP options\n           }\n       }\n\n       return IP4Header(\n           ipVersion: ipVersion, internetHeaderLength: internetHeaderLength, diffTypeOfService: diffTypeOfService, ecn: ecn, totalLength: totalLength, identification: identification,\n           mayFragment: mayFragment, lastFragment: lastFragment, fragmentOffset: fragmentOffset, timeToLive: timeToLive, protocolNumber: protocolNumber, headerChecksum: checksum,\n           sourceIP: sourceIp, destinationIP: desIp\n       )\n   }\n\n   public static func printPacket(data: Data) {\n       guard let ipHeader = createIP4Header(data: data) else {\n           return\n       }\n       os_log(\"IP Header: version: %{public}d, internetHeaderLength: %{public}d, diffTypeOfService: %{public}d, ecn: %{public}d, totalLength: %{public}d, identification: %{public}d, mayFragment: %{public}d, lastFragment: %{public}d, fragmentOffset: %{public}d, timeToLive: %{public}d, protocolNumber: %{public}d, headerChecksum: %{public}d, sourceIP: %{public}@, destinationIP: %{public}@\", log: OSLog.default, type: .default, ipHeader.ipVersion, ipHeader.internetHeaderLength, ipHeader.diffTypeOfService, ipHeader.ecn, ipHeader.totalLength, ipHeader.identification, ipHeader.mayFragment, ipHeader.lastFragment, ipHeader.fragmentOffset, ipHeader.timeToLive, ipHeader.protocolNumber, ipHeader.headerChecksum,  PacketUtil.intToIPAddress(ipHeader.sourceIP),  PacketUtil.intToIPAddress(ipHeader.destinationIP))\n   }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/transport/protocol/TCPHeader.swift",
    "content": "//\n//  TCPHeader.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/16.\n//\n\nimport Foundation\n\n/// Represents a TCP header in a network packet.\nclass TCPHeader : TransportHeader{\n    \n    /// Source port number (16 bits)\n    var sourcePort: UInt16\n    /// Destination port number (16 bits)\n    var destinationPort: UInt16\n    /// Sequence number (32 bits)\n    var sequenceNumber: UInt32\n    /// Acknowledgment number (32 bits)\n    var ackNumber: UInt32\n    /// Data offset (4 bits)\n    var dataOffset: UInt8\n    var isNS: Bool = false // ECN-nonce concealment protection (experimental: see RFC 3540)\n    /// Flags (9 bits)\n    var flags: UInt8\n    /// Window size (16 bits)\n    var windowSize: UInt16\n    /// Checksum (16 bits)\n    var checksum: UInt16\n    /// Urgent pointer (16 bits)\n    var urgentPointer: UInt16\n    /// Options (variable length)\n    var options: Data?\n    var payload: Data?\n\n    //Static section for constants\n    static let END_OF_OPTIONS_LIST: UInt8 = 0\n    static let NO_OPERATION: UInt8 = 1\n    static let MAX_SEGMENT_SIZE: UInt8 = 2\n    static let WINDOW_SCALE: UInt8 = 3\n    static let SELECTIVE_ACK_PERMITTED: UInt8 = 4\n    static let TIME_STAMP: UInt8 = 8\n\n    init(sourcePort: UInt16, destinationPort: UInt16, sequenceNumber: UInt32, ackNumber: UInt32, dataOffset: UInt8, isNS: Bool, flags: UInt8, windowSize: UInt16, checksum: UInt16, urgentPointer: UInt16, options: Data?, payload: Data? = nil) {\n        self.sourcePort = sourcePort\n        self.destinationPort = destinationPort\n        self.sequenceNumber = sequenceNumber\n        self.ackNumber = ackNumber\n        self.dataOffset = dataOffset\n        self.isNS = isNS\n        self.flags = flags\n        self.windowSize = windowSize\n        self.checksum = checksum\n        self.urgentPointer = urgentPointer\n        self.options = options\n        self.payload = payload\n    }\n\n    //options\n    var maxSegmentSize: UInt16 = 0\n    private var windowScale: UInt8 = 0\n    private var isSelectiveAckPermitted = false\n    var timeStampSender = 0\n    var timeStampReplyTo = 0\n\n    func getSourcePort() -> Int {\n        return Int(sourcePort)\n    }\n    \n    func getDestinationPort() -> Int {\n        return Int(destinationPort)\n    }\n    \n    func isFIN() -> Bool {\n        return flags & 0x01 != 0\n    }\n\n    /// Checks if the SYN flag is set.\n    func isSYN() -> Bool {\n        return flags & 0x02 != 0\n    }\n\n    /// Checks if the RST flag is set.\n    func isRST() -> Bool {\n        return flags & 0x04 != 0\n    }\n\n    /// Checks if the PSH flag is set.\n    func isPSH() -> Bool {\n        return flags & 0x08 != 0\n    }\n\n    /// Checks if the ACK flag is set.\n    func isACK() -> Bool {\n        return flags & 0x10 != 0\n    }\n\n    /// Checks if the URG flag is set.\n    func isURG() -> Bool {\n        return flags & 0x20 != 0\n    }\n\n    /// Checks if the ECE flag is set.\n    func isECE() -> Bool {\n        return flags & 0x40 != 0\n    }\n\n    /// Checks if the CWR flag is set.\n    func isCWR() -> Bool {\n        return flags & 0x80 != 0\n    }\n\n    /// Sets or clears the RST flag.\n    func setIsRST(_ isRST: Bool) {\n        flags = isRST ? (flags | 0x04) : (flags & 0xFB)\n    }\n\n    /// Sets or clears the SYN flag.\n    func setIsSYN(_ isSYN: Bool) {\n        flags = isSYN ? (flags | 0x02) : (flags & 0xFD)\n    }\n\n    /// Sets or clears the FIN flag.\n    func setIsFIN(_ isFIN: Bool) {\n        flags = isFIN ? (flags | 0x01) : (flags & 0xFE)\n    }\n\n    /// Sets or clears the PSH flag.\n    func setIsPSH(_ isPSH: Bool) {\n        flags = isPSH ? (flags | 0x08) : (flags & 0xF7)\n    }\n\n    /// Sets or clears the ACK flag.\n    func setIsACK(_ isACK: Bool) {\n        flags = isACK ? (flags | 0x10) : (flags & 0xEF)\n    }\n\n    /// Returns the length of the TCP header.\n    func getTCPHeaderLength() -> Int {\n        return Int(dataOffset) * 4\n    }\n\n    /// Converts the TCP header to a byte array.\n    func toBytes() -> Data {\n        var buffer = Data()\n\n        buffer.append(contentsOf: sourcePort.bytes)\n        buffer.append(contentsOf: destinationPort.bytes)\n        buffer.append(contentsOf: sequenceNumber.bytes)\n        buffer.append(contentsOf: ackNumber.bytes)\n\n        //is ns and data offset\n        let headerLength = 5\n        buffer.append(UInt8((headerLength << 4) | (isNS ? 1 : 0)))\n        buffer.append(flags)\n        buffer.append(contentsOf: windowSize.bytes)\n        buffer.append(contentsOf: checksum.bytes)\n        buffer.append(contentsOf: urgentPointer.bytes)\n//         if let options = options {\n//             buffer.append(options)\n//         }\n        return buffer\n    }\n\n    /// Creates a copy of the TCP header.\n    func copy() -> TCPHeader {\n        return TCPHeader(\n            sourcePort: sourcePort,\n            destinationPort: destinationPort,\n            sequenceNumber: sequenceNumber,\n            ackNumber: ackNumber,\n            dataOffset: dataOffset,\n            isNS: isNS,\n            flags: flags,\n            windowSize: windowSize,\n            checksum: checksum,\n            urgentPointer: urgentPointer,\n            options: options\n        )\n    }\n\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/transport/protocol/TCPPacketFactory.swift",
    "content": "//\n//  TCPPacketFactory.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/16.\n//\n//\n\nimport Foundation\nimport os.log\n\n/// Factory class for creating TCP packets.\nclass TCPPacketFactory {\n\n   public static let TCP_HEADER_LENGTH = 20\n\n   //从tcp报文创建tcpHeader\n    static func createTCPHeader(data: Data) -> TCPHeader? {\n        if  data.count < TCP_HEADER_LENGTH {\n            os_log(\"Data is too short to be a TCP packet\", log: OSLog.default, type: .error)\n            return nil\n        }\n\n\n        var offset = 0\n\n        func readUInt16() -> UInt16 {\n            let value = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt16.self).bigEndian }\n            offset += 2\n            return value\n        }\n\n        func readUInt32() -> UInt32 {\n            let value = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt32.self).bigEndian }\n            offset += 4\n            return value\n        }\n\n        let sourcePort = readUInt16()\n        let destinationPort = readUInt16()\n\n        let sequenceNumber = readUInt32()\n        let ackNumber = readUInt32()\n\n        let dataOffsetAndReserved = data[offset]\n        offset += 1\n        let dataOffset = UInt8((dataOffsetAndReserved & 0xF0) >> 4)\n        let isNs = (dataOffsetAndReserved & 0x01) == 1\n        let flags = UInt8(data[offset])\n        offset += 1\n\n        let windowSize = readUInt16()\n        let checksum = readUInt16()\n        let urgentPointer = readUInt16()\n\n        var optionsSize = Int(dataOffset) - 5\n        var options: Data?\n        if (optionsSize > 0) {\n            optionsSize *= 4\n            options = data.subdata(in: offset..<offset + optionsSize)\n        }\n\n        let payload: Data? = offset < data.count ? data.subdata(in: offset..<data.count) : nil\n        return TCPHeader(\n            sourcePort: sourcePort,\n            destinationPort: destinationPort,\n            sequenceNumber: sequenceNumber,\n            ackNumber: ackNumber,\n            dataOffset: dataOffset,\n            isNS: isNs,\n            flags: flags,\n            windowSize: windowSize,\n            checksum: checksum,\n            urgentPointer: urgentPointer,\n            options: options,\n            payload: payload\n        )\n    }\n    \n    //向客户端确认服务器已收到请求\n    static func createResponseAckData(ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: UInt32) -> Data {\n       var ip = ipHeader.copy()\n       var tcp = tcpHeader.copy()\n\n       flipIp(ip: &ip, tcp: &tcp)\n       let seqNumber = tcp.ackNumber\n       tcp.ackNumber = ackToClient\n       tcp.sequenceNumber = seqNumber\n\n        ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId())\n\n       // Set TCP flags\n       tcp.setIsACK(true)\n       tcp.setIsSYN(false)\n       tcp.setIsPSH(false)\n       tcp.setIsFIN(false)\n\n       tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes\n       tcp.options = nil\n\n       ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength())\n        \n       return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil)\n   }\n    \n    ///创建带有RST标志的数据包，以便在需要重置时发送到客户端。\n    static func createRstData(ipHeader: IP4Header, tcpHeader: TCPHeader, dataLength: Int) -> Data {\n        var ip = ipHeader.copy()\n        var tcp = tcpHeader.copy()\n\n        var ackNumber: UInt32 = 0\n        var seqNumber: UInt32 = 0\n\n        if tcp.ackNumber > 0 {\n            seqNumber = tcp.ackNumber\n        } else {\n            ackNumber = tcp.sequenceNumber + UInt32(dataLength)\n        }\n\n        tcp.ackNumber = ackNumber\n        tcp.sequenceNumber = seqNumber\n\n        // Flip IP from source to destination\n        flipIp(ip: &ip, tcp: &tcp)\n\n        ip.identification = 0\n\n        tcp.flags = 0\n        tcp.isNS = false\n        tcp.setIsRST(true)\n\n        tcp.dataOffset = 5\n        tcp.options = nil\n        tcp.windowSize = 0\n\n        // Recalculate IP length\n        let totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()\n        ip.totalLength = UInt16(totalLength)\n\n        return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil)\n    }\n    \n    //创建发送到客户端的FIN-ACK\n    static func createFinAckData(ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: UInt32, seqToClient: UInt32, isFin: Bool, isAck: Bool) -> Data {\n        var ip = ipHeader.copy()\n        var tcp = tcpHeader.copy()\n\n        flipIp(ip: &ip, tcp: &tcp)\n\n        tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes\n        tcp.options = nil\n\n        tcp.ackNumber = ackToClient\n        tcp.sequenceNumber = seqToClient\n        ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId())\n\n        tcp.setIsACK(isAck)\n        tcp.setIsSYN(false)\n        tcp.setIsPSH(false)\n        tcp.setIsFIN(isFin)\n\n        ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength())\n        return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil)\n    }\n\n   //通过写回客户端流创建SYN-ACK数据包数据\n   public static func createSynAckPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader) -> Packet {\n       var ip = ipHeader.copy()\n       var tcp = tcpHeader.copy()\n       flipIp(ip: &ip, tcp: &tcp)\n\n       tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes\n       tcp.options = nil\n\n       // ack = received sequence + 1\n       let ackNumber = tcpHeader.sequenceNumber + 1\n       tcp.ackNumber = ackNumber\n\n       // Server-generated initial sequence number\n       let seqNumber = UInt64.random(in: 0..<100000)\n       tcp.sequenceNumber = UInt32(seqNumber)\n\n       // SYN-ACK\n       tcp.setIsACK(true)\n       tcp.setIsSYN(true)\n\n       tcp.timeStampReplyTo = tcp.timeStampSender\n       tcp.timeStampSender = PacketUtil.currentTime\n\n       ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength())\n\n       return Packet(ipHeader: ip, transportHeader: tcp, buffer: createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil))\n   }\n\n    //创建数据包数据以发送回客户端\n   public static func createResponsePacketData(\n        ipHeader: IP4Header, tcpHeader: TCPHeader, packetData: Data?, isPsh: Bool,\n        ackNumber: UInt32, seqNumber: UInt32, timeSender: Int, timeReplyTo: Int\n    ) -> Data {\n        var ip = ipHeader.copy()\n        var tcp = tcpHeader.copy()\n\n        flipIp(ip: &ip, tcp: &tcp)\n\n        tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes\n        tcp.options = nil\n\n        tcp.ackNumber = ackNumber\n        tcp.sequenceNumber = seqNumber\n        ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId())\n\n        // ACK is always sent\n        tcp.setIsACK(true)\n        tcp.setIsSYN(false)\n        tcp.setIsPSH(isPsh)\n        tcp.setIsFIN(false)\n        tcp.timeStampSender = timeSender\n        tcp.timeStampReplyTo = timeReplyTo\n\n        var totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()\n        if let packetData = packetData {\n            totalLength += packetData.count\n        }\n        ip.totalLength = UInt16(totalLength)\n\n        return createPacketData(ipHeader: ip, tcpHeader: tcp, data: packetData)\n    }\n\n    //将IP从源翻转到目标\n    private static func flipIp(ip: inout IP4Header, tcp: inout TCPHeader) {\n       let sourceIp = ip.destinationIP\n       let destIp = ip.sourceIP\n       let sourcePort = tcp.destinationPort\n       let destPort = tcp.sourcePort\n\n       ip.destinationIP = destIp\n       ip.sourceIP = sourceIp\n       tcp.destinationPort = destPort\n       tcp.sourcePort = sourcePort\n    }\n\n    public static func createFinData(\n        ipHeader: IP4Header, tcpHeader: TCPHeader, ackNumber: UInt32, seqNumber: UInt32,\n        timeSender: Int, timeReplyTo: Int\n    ) -> Data {\n        var ip = ipHeader.copy()\n        var tcp = tcpHeader.copy()\n\n        flipIp(ip: &ip, tcp: &tcp)\n\n        tcp.ackNumber = ackNumber\n        tcp.sequenceNumber = seqNumber\n\n        ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId())\n\n        tcp.timeStampReplyTo = timeReplyTo\n        tcp.timeStampSender = timeSender\n\n        tcp.flags = 0\n        tcp.isNS = false\n        tcp.setIsACK(true)\n        tcp.setIsFIN(true)\n\n        tcp.options = nil\n        tcp.windowSize = 0\n\n        ip.totalLength = UInt16(ip.getIPHeaderLength() + TCP_HEADER_LENGTH)\n        return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil)\n    }\n    \n\n    //从tcpHeader创建tcp报文\n    private static func createPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader, data: Data?) -> Data {\n       let dataLength = data?.count ?? 0\n\n       var buffer = Data()\n\n       // Add IP header\n       let ipBuffer = ipHeader.toBytes()\n       buffer.append(ipBuffer)\n\n       // Add TCP header\n       let tcpBuffer = tcpHeader.toBytes()\n       buffer.append(tcpBuffer)\n\n       // Add data if exists\n       if let data = data {\n           buffer.append(data)\n       }\n\n       // Zero out IP checksum\n       buffer[10] = 0\n       buffer[11] = 0\n\n       // Calculate IP checksum\n       let ipChecksum = PacketUtil.calculateChecksum(data: buffer, offset: 0, length: ipBuffer.count)\n       buffer[10] = ipChecksum[0]\n       buffer[11] = ipChecksum[1]\n//       IPPacketFactory.printPacket(data: ipBuffer)\n\n       // Zero out TCP checksum\n       let tcpStart = ipBuffer.count\n       buffer[tcpStart + 16] = 0\n       buffer[tcpStart + 17] = 0\n\n       // Calculate TCP checksum\n       let tcpChecksum = PacketUtil.calculateTCPHeaderChecksum(\n        data: buffer, offset: tcpStart, tcpLength: tcpBuffer.count + dataLength,\n           sourceIP: ipHeader.sourceIP, destinationIP: ipHeader.destinationIP\n       )\n       buffer[tcpStart + 16] = tcpChecksum[0]\n       buffer[tcpStart + 17] = tcpChecksum[1]\n       return buffer\n   }\n\n    static func printPacket(data: Data) {\n        guard let tcpHeader = createTCPHeader(data: data) else {\n            os_log(\"Failed to create TCP header\", log: OSLog.default, type: .error)\n            return\n        }\n\n        os_log(\"TCP Header: sourcePort: %{public}d, destinationPort: %{public}d, sequenceNumber: %{public}u, ackNumber: %{public}u, dataOffset: %{public}d, isNS: %{public}d, flags: %{public}d, windowSize: %{public}d, checksum: %{public}u, urgentPointer: %{public}u\",\n               log: OSLog.default, type: .default, tcpHeader.sourcePort, tcpHeader.destinationPort, tcpHeader.sequenceNumber, tcpHeader.ackNumber, tcpHeader.dataOffset, tcpHeader.isNS ? 1 : 0, tcpHeader.flags, tcpHeader.windowSize, tcpHeader.checksum, tcpHeader.urgentPointer)\n    }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/transport/protocol/TransportHeader.swift",
    "content": "//\n//  TransportHeader.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/17.\n//\n\nimport Foundation\n\nprotocol TransportHeader {\n    func getSourcePort() -> Int\n    func getDestinationPort() -> Int\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/transport/protocol/UDPHeader.swift",
    "content": "//\n//  UDPHeader.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/17.\n//\n\nimport Foundation\nimport os.log\n\n///UDP报头的数据\nstruct UDPHeader {\n    var sourcePort: UInt16  //源端口号 16bit\n    var destinationPort: UInt16  //源端口号 16bit\n    var length: UInt16  //UDP数据报长度 16bit\n    var checksum: UInt16 //校验和 16bit\n\n    init(sourcePort: UInt16, destinationPort: UInt16, length: UInt16, checksum: UInt16) {\n        self.sourcePort = sourcePort\n        self.destinationPort = destinationPort\n        self.length = length\n        self.checksum = checksum\n    }\n}\n\nclass UDPPacketFactory {\n    static let UDP_HEADER_LENGTH = 8\n    \n    static func createUDPHeader(from data: Data) -> UDPHeader? {\n        guard data.count >= UDP_HEADER_LENGTH else {\n            return nil\n        }\n\n        let srcPort = data.withUnsafeBytes { $0.load(fromByteOffset: 0, as: UInt16.self).bigEndian }\n        let destPort = data.withUnsafeBytes { $0.load(fromByteOffset: 2, as: UInt16.self).bigEndian }\n        let length = data.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt16.self).bigEndian }\n        let checksum = data.withUnsafeBytes { $0.load(fromByteOffset: 6, as: UInt16.self).bigEndian }\n\n        return UDPHeader(sourcePort: srcPort, destinationPort: destPort, length: length, checksum: checksum)\n    }\n    \n\n    //\n    static func createResponsePacket(ip: IP4Header, udp: UDPHeader, packetData: Data?) -> Data {\n        var udpLen = 8\n        if let packetData = packetData {\n            udpLen += packetData.count\n        }\n        \n        let srcPort = udp.destinationPort\n        let destPort = udp.sourcePort\n\n        let ipHeader = ip.copy()\n        let srcIp = ip.destinationIP\n        let destIp = ip.sourceIP\n\n        ipHeader.setMayFragment(false)\n        ipHeader.sourceIP = srcIp\n        ipHeader.destinationIP = destIp\n        ipHeader.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId())\n\n        //ip的长度是整个数据包的长度 => IP header length + UDP header length (8) + UDP body length\n        let totalLength = ipHeader.getIPHeaderLength() + udpLen\n        ipHeader.totalLength = UInt16(totalLength)\n\n        var ipData = ipHeader.toBytes()\n        \n        // clear IP checksum\n        ipData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in\n            bytes[10] = 0\n            bytes[11] = 0\n        }\n\n        // calculate checksum for IP header\n        let ipChecksum = PacketUtil.calculateChecksum(data: ipData, offset: 0, length: ipData.count)\n\n        // write result of checksum back to buffer\n        ipData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in\n            bytes[10] = ipChecksum[0]\n            bytes[11] = ipChecksum[1]\n        }\n\n        var buffer = Data()\n\n        // copy IP header to buffer\n        buffer.append(ipData)\n\n        // copy UDP header to buffer\n        buffer.append(contentsOf: srcPort.bytes)\n        buffer.append(contentsOf: destPort.bytes)\n        buffer.append(contentsOf: UInt16(udpLen).bytes)\n\n        // 计算UDP校验和\n        let udpChecksum: UInt16 = 0\n        buffer.append(contentsOf: udpChecksum.bytes)\n\n        if let packetData = packetData {\n            buffer.append(packetData)\n        }\n        return buffer\n    }\n\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/utils/PacketUtil.swift",
    "content": "//\n//  PacketUtil.swift\n//  ProxyPin\n//\n//  Created by wanghongen on 2024/9/17.\n//\n\nimport Foundation\nimport os.log\n\nclass PacketUtil {\n    private static var packetId: Int = 0\n\n    static func getPacketId() -> Int {\n        defer { packetId += 1 }\n        return packetId\n    }\n    \n    static var currentTime: Int {\n        return Int(Date().timeIntervalSince1970)\n    }\n    \n   static func writeIntToBytes(value: UInt32, buffer: inout Data, offset: Int) {\n        guard buffer.count >= offset + 4 else { return }\n        var intValue = value.bigEndian\n        let intData = Data(bytes: &intValue, count: 4)\n        buffer.replaceSubrange(offset..<offset+4, with: intData)\n    }\n    \n    static func intToIPAddress(_ ip: UInt32) -> String {\n        return String(format: \"%d.%d.%d.%d\", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF)\n    }\n    \n    static func calculateTCPHeaderChecksum(data: Data, offset: Int, tcpLength: Int, sourceIP: UInt32, destinationIP: UInt32) -> Data {\n        var bufferSize = tcpLength + 12\n        var isOdd = false\n        if bufferSize % 2 != 0 {\n            bufferSize += 1\n            isOdd = true\n        }\n\n        var buffer = Data()\n\n        // Add source IP\n        buffer.append(contentsOf: sourceIP.bytes)\n        // Add destination IP\n        buffer.append(contentsOf: destinationIP.bytes)\n\n        // Add reserved byte and protocol (6 for TCP)\n        buffer.append(0)\n        buffer.append(6)\n\n        // Add TCP length\n        buffer.append(contentsOf: UInt16(tcpLength).bytes)\n\n        // Add TCP header and data\n        buffer.append(contentsOf: data[offset..<offset + tcpLength])\n\n        // Pad with zero if odd length\n        if isOdd {\n            buffer.append(0)\n        }\n\n        // Calculate checksum\n        return calculateChecksum(data: buffer, offset: 0, length: bufferSize)\n    }\n\n    static func calculateChecksum(data: Data, offset: Int, length: Int) -> Data {\n        var start = offset\n        var sum = 0\n\n        while start < length {\n            sum += getNetworkInt(buffer: data, start: start, length: 2)\n            start += 2\n        }\n\n        // Carry over one's complement\n        while (sum >> 16) > 0 {\n            sum = (sum & 0xFFFF) + (sum >> 16)\n        }\n\n        // Flip the bits to get one's complement\n        sum = ~sum\n\n        // Extract the last two bytes of the int\n        let checksum = Data([UInt8(truncatingIfNeeded: (sum >> 8) & 0xFF), UInt8(truncatingIfNeeded: sum & 0xFF)])\n        return checksum\n    }\n    \n    static func getNetworkInt(buffer: Data, start: Int, length: Int) -> Int {\n        var value = 0\n        var end = start + min(length, 4)\n        if end > buffer.count { end = buffer.count }\n        for i in start..<end {\n            value = value | (Int(buffer[i]) & 0xFF)\n            if i < end - 1 { value = value << 8 }\n        }\n        return value\n    }\n    \n    static func isPacketCorrupted(tcpHeader: TCPHeader) -> Bool {\n        guard let options = tcpHeader.options else {\n            return false\n        }\n\n        var i = 0\n        while i < options.count {\n            let kind = options[i]\n            switch kind {\n            case 0, 1:\n                break\n            case 2:\n                i += 3\n            case 3, 14:\n                i += 2\n            case 4:\n                i += 1\n            case 5, 15:\n                i += Int(options[i + 1]) - 2\n            case 8:\n                i += 9\n            case 23:\n                return true\n            default:\n                print(\"Unknown option: \\(kind)\")\n            }\n            i += 1\n        }\n        return false\n    }\n}\n\n\nextension FixedWidthInteger {\n    var bytes: [UInt8] {\n        withUnsafeBytes(of: self.bigEndian) { Array($0) }\n    }\n}\n"
  },
  {
    "path": "ios/ProxyPin/vpn/utils/TLS.swift",
    "content": "//\n//  TLS.swift\n//  Runner\n//\n//  Created by wanghongen on 2025/5/31.\n//\n\nclass TLS {\n    \n    static func isTLSClientHello(packetData: Data) -> Bool {\n        // Ensure the packet has enough data for a TLS ClientHello message\n        guard packetData.count >= 43 else {\n            return false\n        }\n\n        // Check if the first byte is 0x16 (Handshake type: ClientHello)\n        if packetData[0] != 0x16 {\n            return false\n        }\n\n        // Check if the next two bytes represent a valid TLS version (e.g., 0x0301, 0x0302, 0x0303)\n        let version = packetData[1...2]\n        if version != Data([0x03, 0x01]) && version != Data([0x03, 0x02]) && version != Data([0x03, 0x03]) {\n            return false\n        }\n\n        // Check if the handshake message type is ClientHello (0x01)\n        if packetData[5] != 0x01 {\n            return false\n        }\n\n        // Check if the record layer protocol version matches the expected TLS version\n        let recordVersion = packetData[9...10]\n        if recordVersion != Data([0x03, 0x01]) && recordVersion != Data([0x03, 0x02]) && recordVersion != Data([0x03, 0x03]) {\n            return false\n        }\n\n        return true\n    }\n}\n"
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport Flutter\nimport NetworkExtension\n\n@main\n@objc class AppDelegate: FlutterAppDelegate {\n    \n    var backgroundAudioEnable: Bool = true\n    \n    override func application(_ application: UIApplication,\n                              didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\n        GeneratedPluginRegistrant.register(with: self)\n\n        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController\n        let vpnChannel = FlutterMethodChannel.init(name: \"com.proxy/proxyVpn\", binaryMessenger: controller as! FlutterBinaryMessenger);\n            vpnChannel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in\n                if (\"stopVpn\" == call.method) {\n                    VpnManager.shared.disconnect()\n                } else if (\"isRunning\" == call.method){\n                    result(Bool(VpnManager.shared.isRunning()))\n                } else if (\"restartVpn\" == call.method){\n                    let arguments = call.arguments as? Dictionary<String, AnyObject>\n//                     VpnManager.shared.disconnect()\n                    VpnManager.shared.restartConnect(host: arguments?[\"proxyHost\"] as? String ,port: arguments?[\"proxyPort\"] as? Int, ipProxy: arguments?[\"ipProxy\"] as? Bool, proxyPassDomains: arguments?[\"proxyPassDomains\"] as? [String])\n                } else {\n                    let arguments = call.arguments as? Dictionary<String, AnyObject>\n                    VpnManager.shared.connect(host: arguments?[\"proxyHost\"] as? String ,port: arguments?[\"proxyPort\"] as? Int, ipProxy: arguments?[\"ipProxy\"] as? Bool, proxyPassDomains: arguments?[\"proxyPassDomains\"] as? [String])\n              }\n          })\n      \n        if #available(iOS 13.0.0, *) {\n            PictureInPictureManager.regirst(flutter: controller as! FlutterBinaryMessenger)\n            MethodHandler.register(with: self.registrar(forPlugin: MethodHandler.name)!)\n        }\n        \n        if let window = self.window {\n            window.rootViewController = controller\n        }\n\n        return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n   }\n\n    override func applicationWillTerminate(_ application: UIApplication) {\n        VpnManager.shared.disconnect()\n    }\n\n    var timer: Timer?\n    var bgTask: UIBackgroundTaskIdentifier?\n\n    override func applicationDidEnterBackground(_ application: UIApplication) {\n        if (!VpnManager.shared.isRunning()) {\n            return\n        }\n    \n        timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)\n        RunLoop.current.add(timer!, forMode: RunLoop.Mode.common)\n               bgTask = application.beginBackgroundTask(expirationHandler: nil)\n    }\n\n    @objc func timerAction() {\n        print(UIApplication.shared.backgroundTimeRemaining)\n        let application = UIApplication.shared\n        \n        if (bgTask != nil) {\n            application.endBackgroundTask(bgTask!);\n            bgTask = nil;\n        }\n        \n        if (UIApplication.shared.backgroundTimeRemaining < 60 && VpnManager.shared.isRunning()) {\n            bgTask = application.beginBackgroundTask(expirationHandler: nil)\n        }\n            \n        if (application.backgroundTimeRemaining <= 0 || application.applicationState == .active || AudioManager.shared.openBackgroundAudioAutoplay) {\n            timer?.invalidate();\n            timer = nil;\n        }\n        \n        if (application.backgroundTimeRemaining <= 10) {\n            self.backgroundAudio()\n        }\n\n    }\n\n    override func applicationWillResignActive(_ application: UIApplication) {\n        self.backgroundAudio();\n    }\n    override  func applicationDidBecomeActive(_ application: UIApplication) {\n        self.endBackgroundUpdateTask()\n    }\n    \n    private func backgroundAudio() {\n        if (!VpnManager.shared.isRunning() || !self.backgroundAudioEnable) {\n            return\n        }\n        if (AudioManager.shared.openBackgroundAudioAutoplay) {\n            return;\n        }\n        \n        AudioManager.shared.openBackgroundAudioAutoplay = true\n        self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {\n            self.endBackgroundUpdateTask()\n        })\n    }\n    \n    var backgroundUpdateTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier(rawValue: 0)\n    \n    func endBackgroundUpdateTask() {\n        if (!VpnManager.shared.isRunning() || !AudioManager.shared.openBackgroundAudioAutoplay) {\n            return\n        }\n\n        AudioManager.shared.openBackgroundAudioAutoplay = false\n        UIApplication.shared.endBackgroundTask(self.backgroundUpdateTask)\n        self.backgroundUpdateTask = UIBackgroundTaskIdentifier.invalid\n    }\n\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\": [\n    {\n      \"filename\": \"AppIcon@2x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"2x\",\n      \"size\": \"60x60\"\n    },\n    {\n      \"filename\": \"AppIcon@3x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"3x\",\n      \"size\": \"60x60\"\n    },\n    {\n      \"filename\": \"AppIcon~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"1x\",\n      \"size\": \"76x76\"\n    },\n    {\n      \"filename\": \"AppIcon-40@2x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"2x\",\n      \"size\": \"40x40\"\n    },\n    {\n      \"filename\": \"AppIcon-40@3x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"3x\",\n      \"size\": \"40x40\"\n    },\n    {\n      \"filename\": \"AppIcon-20@2x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"2x\",\n      \"size\": \"20x20\"\n    },\n    {\n      \"filename\": \"AppIcon-20@3x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"3x\",\n      \"size\": \"20x20\"\n    },\n    {\n      \"filename\": \"AppIcon-29.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"1x\",\n      \"size\": \"29x29\"\n    },\n    {\n      \"filename\": \"AppIcon-29@2x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"2x\",\n      \"size\": \"29x29\"\n    },\n    {\n      \"filename\": \"AppIcon-29@3x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"3x\",\n      \"size\": \"29x29\"\n    },\n    {\n      \"filename\": \"AppIcon-60@2x~car.png\",\n      \"idiom\": \"car\",\n      \"scale\": \"2x\",\n      \"size\": \"60x60\"\n    },\n    {\n      \"filename\": \"AppIcon-60@3x~car.png\",\n      \"idiom\": \"car\",\n      \"scale\": \"3x\",\n      \"size\": \"60x60\"\n    },\n    {\n      \"filename\": \"AppIcon@2x~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"2x\",\n      \"size\": \"76x76\"\n    },\n    {\n      \"filename\": \"AppIcon-83.5@2x~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"2x\",\n      \"size\": \"83.5x83.5\"\n    },\n    {\n      \"filename\": \"AppIcon~ios-marketing.png\",\n      \"idiom\": \"ios-marketing\",\n      \"scale\": \"1x\",\n      \"size\": \"1024x1024\"\n    }\n  ]\n}"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "ios/Runner/AudioManager.swift",
    "content": "import Foundation\nimport AVFoundation\nimport UIKit\nclass AudioManager: NSObject {\n    \n    static let shared = AudioManager()\n    fileprivate let audioSession = AVAudioSession.sharedInstance()\n    fileprivate var backgroundAudioPlayer: AVAudioPlayer?\n    fileprivate var backgroundTimeLength = 0\n    fileprivate var timer: Timer?\n    \n    static let audioName = \"\"\n    // 是否开启后台自动播放无声音乐\n    var openBackgroundAudioAutoplay = false {\n        didSet {\n            if self.openBackgroundAudioAutoplay {\n                self.setupAudioSession()\n                self.setupBackgroundAudioPlayer()\n            } else {\n                if let player = self.backgroundAudioPlayer {\n                    if player.isPlaying {\n                        player.stop()\n                    }\n                }\n                self.backgroundAudioPlayer = nil\n                try? self.audioSession.setActive(false, options:  AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation)\n            }\n        }\n    }\n    \n    override init() {\n        super.init()\n        self.setupListener()\n    }\n    deinit {\n        NotificationCenter.default.removeObserver(self)\n    }\n    private func setupAudioSession() {\n        do {\n            try self.audioSession.setCategory(AVAudioSession.Category.playback, options: AVAudioSession.CategoryOptions.mixWithOthers)\n            try self.audioSession.setActive(false)\n        } catch let error {\n            debugPrint(\"\\(type(of:self)):\\(error)\")\n        }\n    }\n    private func setupBackgroundAudioPlayer() {\n        do {\n            self.backgroundAudioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: Bundle.main.path(forResource: \"silence\", ofType: \"mp3\")!))\n        } catch let error {\n            debugPrint(\"\\(type(of:self)):\\(error)\")\n        }\n        self.backgroundAudioPlayer?.numberOfLoops = -1\n        self.backgroundAudioPlayer?.volume = 0\n        self.backgroundAudioPlayer?.delegate = self\n    }\n    \n    private func setupListener() {\n        \n        NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)\n        NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)\n        \n        NotificationCenter.default.addObserver(self, selector: #selector(audioSessionInterruption(notification:)), name: AVAudioSession.interruptionNotification, object: nil)\n\n    }\n}\n\n// MARK: - 扩展 监听通知\nextension AudioManager {\n    /// 进入后台 播放无声音乐\n    @objc public func didEnterBackground() {\n        self.setupTimer()\n        guard self.openBackgroundAudioAutoplay else {return}\n        \n        do {\n            try self.audioSession.setActive(true)\n        } catch let error {\n            debugPrint(\"\\(type(of:self)):\\(error))\")\n        }\n        self.backgroundAudioPlayer?.prepareToPlay()\n        self.backgroundAudioPlayer?.play()\n    }\n    \n    /// 进入前台，暂停播放音乐\n    @objc public func didBecomeActive() {\n        self.removeTimer()\n        self.hintBackgroundTimeLength()\n        self.backgroundTimeLength = 0\n        guard self.openBackgroundAudioAutoplay else {return}\n        \n        self.backgroundAudioPlayer?.pause()\n        do {\n            try self.audioSession.setActive(false, options:  AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation)\n        } catch let error {\n            debugPrint(\"\\(type(of:self)):\\(error))\")\n        }\n        \n        \n    }\n    \n    /// 音乐中断处理\n    @objc fileprivate func audioSessionInterruption(notification: NSNotification) {\n        guard self.openBackgroundAudioAutoplay else {return}\n        guard let userinfo = notification.userInfo else {return}\n        guard let interruptionType: UInt = userinfo[AVAudioSessionInterruptionTypeKey] as! UInt?  else {return}\n        if interruptionType == AVAudioSession.InterruptionType.began.rawValue {\n            // 中断开始，音乐被暂停\n            debugPrint(\"\\(type(of:self)): 中断开始 userinfo:\\(userinfo)\")\n        } else if interruptionType == AVAudioSession.InterruptionType.ended.rawValue {\n            // 中断结束，恢复播放\n            debugPrint(\"\\(type(of:self)): 中断结束 userinfo:\\(userinfo)\")\n            guard let player = self.backgroundAudioPlayer else {return}\n            if player.isPlaying == false {\n                debugPrint(\"\\(type(of:self)): 音乐未播放，准备开始播放\")\n                do {\n                    try self.audioSession.setActive(true)\n                } catch let error {\n                    debugPrint(\"\\(type(of:self)):\\(error)\")\n                }\n                player.prepareToPlay()\n                player.play()\n            } else {\n                debugPrint(\"\\(type(of:self)): 音乐正在播放\")\n            }\n        }\n    }\n}\n\n// MARK: - 扩展 定时器任务\nextension AudioManager {\n    fileprivate func setupTimer() {\n        self.removeTimer()\n        self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerTask), userInfo: nil, repeats: true)\n        RunLoop.main.add(self.timer!, forMode: RunLoop.Mode.common)\n//         RunLoop.main.add(self.timer!, forMode: RunLoop.Mode.init(rawValue: \"\"))\n    }\n    fileprivate func removeTimer() {\n        self.timer?.invalidate()\n        self.timer = nil;\n    }\n    @objc func timerTask() {\n        self.backgroundTimeLength += 1\n    }\n    fileprivate func hintBackgroundTimeLength() {\n        let message = \"本次后台持续时间:\\(self.backgroundTimeLength)s\"\n        print(message)\n    }\n}\n// MARK: - 扩展 播放代理\nextension AudioManager: AVAudioPlayerDelegate {\n    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {\n        \n    }\n    func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {\n        debugPrint(\"\\(type(of:self))\" + error.debugDescription)\n    }\n}\n"
  },
  {
    "path": "ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"12121\" systemVersion=\"16G29\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"12089\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n                            </imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"1a2-6s-vTC\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"4X2-HB-R7a\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"168\" height=\"185\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"10117\" systemVersion=\"15F34\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"10085\"/>\n    </dependencies>\n    <scenes>\n        <!--Flutter View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"FlutterViewController\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"y3c-jy-aDJ\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"wfy-db-euE\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"8bC-Xf-vdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"600\" height=\"600\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"calibratedWhite\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "ios/Runner/Handlers/MethodHandler.swift",
    "content": "//\n//  MethodHandler.swift\n//  Runner\n//\n//  Created by wanghongen on 2025/5/30.\n//\n\nimport Flutter\nimport Network\nimport SystemConfiguration.CaptiveNetwork\nimport Security\n\npublic class MethodHandler: NSObject, FlutterPlugin {\n    public static let name = \"com.proxypin/method\"\n\n    private var channel: FlutterMethodChannel?\n    private var currentPathMonitor: NWPathMonitor?\n    private var currentCompletionHandler: ((Bool) -> Void)?\n\n    public static func register(with registrar: FlutterPluginRegistrar) {\n        let channel = FlutterMethodChannel(name: Self.name, binaryMessenger: registrar.messenger())\n        let instance = MethodHandler()\n        registrar.addMethodCallDelegate(instance, channel: channel)\n        instance.channel = channel\n    }\n\n    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {\n        switch call.method {\n        case \"requestLocalNetwork\":\n            // 调用异步函数，并在其完成时传递结果\n            self.requestLocalNetworkAccess { isAvailable in\n                print(\"[MethodHandler] requestLocalNetwork result: \\(isAvailable)\")\n                result(isAvailable)\n            }\n        case \"isCaInstalled\":\n            guard let args = call.arguments as? [String: Any], let pem = args[\"pem\"] as? String else {\n                print(\"[MethodHandler] isCaInstalled ARG_ERROR: missing pem\")\n                result(FlutterError(code: \"ARG_ERROR\", message: \"Missing pem\", details: nil))\n                return\n            }\n            let ret = self.isCertificateInstalled(pem: pem)\n            result(ret)\n        case \"evaluateChainTrusted\":\n            guard let args = call.arguments as? [String: Any], let leafPem = args[\"leafPem\"] as? String, let caPem = args[\"caPem\"] as? String else {\n                print(\"[MethodHandler] evaluateChainTrusted ARG_ERROR: missing leafPem/caPem\")\n                result(FlutterError(code: \"ARG_ERROR\", message: \"Missing leafPem/caPem\", details: nil))\n                return\n            }\n            let host = args[\"host\"] as? String\n            let ret = self.isChainTrusted(leafPem: leafPem, caPem: caPem, host: host)\n//             print(\"[MethodHandler] evaluateChainTrusted => \\(ret)\")\n            result(ret)\n        default:\n            print(\"[MethodHandler] method not implemented: \\(call.method)\")\n            result(FlutterMethodNotImplemented)\n        }\n    }\n\n    // MARK: - iOS: Check certificate trust\n    private func isCertificateInstalled(pem: String) -> Bool {\n        guard let der = self.decodePemToDer(pem) as CFData?, let certificate = SecCertificateCreateWithData(nil, der) else {\n            print(\"[MethodHandler] isCertificateTrusted decode/create cert failed\")\n            return false\n        }\n        let policy = SecPolicyCreateBasicX509()\n        var trust: SecTrust?\n        let status = SecTrustCreateWithCertificates(certificate, policy, &trust)\n        if status != errSecSuccess || trust == nil {\n            print(\"[MethodHandler] SecTrustCreateWithCertificates failed status=\\(status)\")\n            return false\n        }\n        if #available(iOS 12.0, *) {\n            var error: CFError?\n            let ok = SecTrustEvaluateWithError(trust!, &error)\n            if let e = error {\n                print(\"[MethodHandler] SecTrustEvaluateWithError ok=\\(ok) error=\\(e)\")\n            }\n            return ok\n        } else {\n            var trustResult = SecTrustResultType.invalid\n            let evalStatus = SecTrustEvaluate(trust!, &trustResult)\n            let ok = (evalStatus == errSecSuccess) && (trustResult == .unspecified || trustResult == .proceed)\n            print(\"[MethodHandler] SecTrustEvaluate status=\\(evalStatus) result=\\(trustResult.rawValue) trusted=\\(ok)\")\n            return ok\n        }\n    }\n\n    // MARK: - iOS: Evaluate leaf+CA chain with SSL policy\n    private func isChainTrusted(leafPem: String, caPem: String, host: String?) -> Bool {\n        guard let leafDer = self.decodePemToDer(leafPem) as CFData?, let leaf = SecCertificateCreateWithData(nil, leafDer) else {\n            print(\"[MethodHandler] isChainTrusted leaf decode/create failed\")\n            return false\n        }\n        guard let caDer = self.decodePemToDer(caPem) as CFData?, let ca = SecCertificateCreateWithData(nil, caDer) else {\n            print(\"[MethodHandler] isChainTrusted ca decode/create failed\")\n            return false\n        }\n        let certs: [SecCertificate] = [leaf, ca]\n        let policy = SecPolicyCreateSSL(true, host as CFString?)\n        var trust: SecTrust?\n        let status = SecTrustCreateWithCertificates(certs as CFTypeRef, policy, &trust)\n        if status != errSecSuccess || trust == nil {\n            print(\"[MethodHandler] isChainTrusted SecTrustCreateWithCertificates failed status=\\(status)\")\n            return false\n        }\n        if #available(iOS 12.0, *) {\n            var error: CFError?\n            let ok = SecTrustEvaluateWithError(trust!, &error)\n            if let e = error { print(\"[MethodHandler] isChainTrusted evaluate ok=\\(ok) error=\\(e)\") } else { print(\"[MethodHandler] isChainTrusted evaluate ok=\\(ok)\") }\n            return ok\n        } else {\n            var trustResult = SecTrustResultType.invalid\n            let evalStatus = SecTrustEvaluate(trust!, &trustResult)\n            let ok = (evalStatus == errSecSuccess) && (trustResult == .unspecified || trustResult == .proceed)\n//             print(\"[MethodHandler] isChainTrusted evaluate status=\\(evalStatus) result=\\(trustResult.rawValue) trusted=\\(ok)\")\n            return ok\n        }\n    }\n\n    private func decodePemToDer(_ pem: String) -> Data? {\n        // Strip header/footer and whitespace\n        let lines = pem.components(separatedBy: \"\\n\").filter { line in\n            return !line.contains(\"-----BEGIN\") && !line.contains(\"-----END\") && !line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty\n        }\n        let base64Str = lines.joined()\n        let der = Data(base64Encoded: base64Str, options: .ignoreUnknownCharacters)\n        return der\n    }\n\n    /// 异步检查本地网络（Wi-Fi 或以太网）是否可用。\n    /// - Parameter completion: 一个回调函数，当检查完成时调用，参数为 Bool 类型，true 表示本地网络可用，false 表示不可用。\n    func requestLocalNetworkAccess(completion: @escaping (Bool) -> Void) {\n\n        // 如果已有正在进行的监视，先取消它\n        self.currentPathMonitor?.cancel()\n\n        self.currentPathMonitor = NWPathMonitor()\n        // 将 completion 存储起来，以便在 pathUpdateHandler 中调用\n        // 这是为了确保 completion 只被调用一次\n        self.currentCompletionHandler = completion\n\n        self.currentPathMonitor?.pathUpdateHandler = { [weak self] path in\n            guard let self = self else { return }\n\n            // 确保 completionHandler 仍然存在（即尚未被调用和清除）\n            guard let completionHandler = self.currentCompletionHandler else {\n                // 可能已经被调用过了，或者监视器被意外触发\n                // 为安全起见，取消监视器\n                self.currentPathMonitor?.cancel()\n                return\n            }\n\n            var isLocalNetworkAvailable = false\n            print(\"Network path status: \\(path.status)\")\n            if path.status == .satisfied {\n                if path.usesInterfaceType(.wifi) || path.usesInterfaceType(.wiredEthernet) {\n                    isLocalNetworkAvailable = true\n                }\n            }\n            // 对于其他状态 (例如 .unsatisfied, .requiresConnection) 或其他接口类型 (例如 cellular),\n            // isLocalNetworkAvailable 将保持 false。\n\n            // 调用存储的 completion handler\n            completionHandler(isLocalNetworkAvailable)\n\n            // 清理：取消监视器并清除存储的引用，以防止重复调用和内存泄漏\n            self.currentPathMonitor?.cancel()\n            self.currentPathMonitor = nil\n            self.currentCompletionHandler = nil\n        }\n\n        // 在主队列上启动监视器\n        self.currentPathMonitor?.start(queue: DispatchQueue.global())\n    }\n}\n"
  },
  {
    "path": "ios/Runner/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>BGTaskSchedulerPermittedIdentifiers</key>\n\t<array>\n\t\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t</array>\n\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>ProxyPin</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>CFBundleLocalizations</key>\n\t<array>\n\t\t<string>en</string>\n\t\t<string>zh</string>\n\t</array>\n\t<key>CFBundleName</key>\n\t<string>ProxyPin</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>NSAppTransportSecurity</key>\n\t<dict>\n\t\t<key>NSAllowsArbitraryLoads</key>\n\t\t<true/>\n\t</dict>\n\t<key>NSCameraUsageDescription</key>\n\t<string>Scan QR code</string>\n\t<key>NSPhotoLibraryUsageDescription</key>\n\t<string>Access to Photo Library</string>\n\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t<true/>\n\t<key>UIBackgroundModes</key>\n\t<array>\n\t\t<string>audio</string>\n\t</array>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\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>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t<false/>\n\t<key>NSAppTransportSecurity</key>\n    <dict>\n      <key>NSAllowsArbitraryLoads</key>\n      <true/>\n    </dict>\n    <key>NSLocalNetworkUsageDescription</key>\n    <string>Remote Device Connect</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "ios/Runner/Runner.entitlements",
    "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>aps-environment</key>\n\t<string>development</string>\n\t<key>com.apple.developer.networking.networkextension</key>\n\t<array>\n\t\t<string>packet-tunnel-provider</string>\n\t</array>\n\t<key>com.apple.developer.networking.vpn.api</key>\n\t<array>\n\t\t<string>allow-vpn</string>\n\t</array>\n\t<key>com.apple.security.application-groups</key>\n\t<array>\n\t\t<string>group.com.proxy.pin</string>\n\t</array>\n\t<key>com.apple.security.network.client</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/VpnManager.swift",
    "content": "let kProxyServiceVPNStatusNotification = \"kProxyServiceVPNStatusNotification\"\n\nimport Foundation\nimport NetworkExtension\n\nenum VPNStatus {\n    case off\n    case connecting\n    case on\n    case disconnecting\n}\n\n\nclass VpnManager{\n    var activeVPN: NETunnelProviderManager?;\n    \n    public var proxyHost: String = \"127.0.0.1\"\n    public var proxyPort: Int = 9099\n    public var ipProxy: Bool = false\n    public var proxyPassDomains: [String]?\n\n    static let shared = VpnManager()\n    var observerAdded: Bool = false\n\n\n    fileprivate(set) var vpnStatus = VPNStatus.off {\n        didSet {\n            NotificationCenter.default.post(name: Notification.Name(rawValue: kProxyServiceVPNStatusNotification), object: nil)\n        }\n    }\n\n    init() {\n        loadProviderManager{\n            guard let manager = $0 else{return}\n            self.updateVPNStatus(manager)\n        }\n        addVPNStatusObserver()\n    }\n\n    deinit {\n        NotificationCenter.default.removeObserver(self)\n    }\n\n    func addVPNStatusObserver() {\n        guard !observerAdded else{\n            return\n        }\n        loadProviderManager { [unowned self] (manager) -> Void in\n            if let manager = manager {\n                self.observerAdded = true\n                NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: manager.connection, queue: OperationQueue.main, using: { [unowned self] (notification) -> Void in\n                    \n                    self.updateVPNStatus(manager)\n                    \n                    if (manager.connection.status == .invalid || manager.connection.status == .disconnected){\n                       \n                        print(\"VPN断开: \\(String(describing: manager.debugDescription))\")\n                    }\n                })\n            }\n        }\n    }\n\n\n    func updateVPNStatus(_ manager: NEVPNManager) {\n        switch manager.connection.status {\n        case .connected:\n            self.vpnStatus = .on\n        case .connecting, .reasserting:\n            self.vpnStatus = .connecting\n        case .disconnecting:\n            self.vpnStatus = .disconnecting\n        case .disconnected, .invalid:\n            self.vpnStatus = .off\n        @unknown default: break\n\n        }\n    }\n}\n\n// load VPN Profiles\nextension VpnManager{\n\n    fileprivate func createProviderManager() -> NETunnelProviderManager {\n        let manager = NETunnelProviderManager()\n        let conf = NETunnelProviderProtocol()\n        conf.serverAddress = \"ProxyPin\"\n        manager.protocolConfiguration = conf\n        manager.localizedDescription = \"ProxyPin\"\n        return manager\n    }\n\n    func loadAndCreatePrividerManager(_ complete: @escaping (NETunnelProviderManager?) -> Void ){\n        NETunnelProviderManager.loadAllFromPreferences{ [self] (managers, error) in\n            guard let managers = managers else{return}\n            let manager: NETunnelProviderManager\n            if managers.count > 0 {\n                manager = managers[0]\n            }else{\n                manager = self.createProviderManager()\n            }\n   \n            var conf = [String:AnyObject]()\n            conf[\"proxyHost\"] = self.proxyHost as AnyObject\n            conf[\"proxyPort\"] = self.proxyPort as AnyObject\n            conf[\"ipProxy\"] = self.ipProxy as AnyObject\n            // Bridge Swift [String] to NSArray (Objective-C) before inserting into AnyObject dictionary\n            if let passDomains = self.proxyPassDomains {\n                conf[\"proxyPassDomains\"] = passDomains as NSArray\n            }\n\n            let orignConf = manager.protocolConfiguration as! NETunnelProviderProtocol\n \n            orignConf.providerConfiguration = conf\n            manager.protocolConfiguration = orignConf\n            \n            print(orignConf)\n            manager.isEnabled = true\n            manager.saveToPreferences{\n                if ($0 != nil){\n//                    complete(nil);\n//                    return;\n                }\n                manager.loadFromPreferences{\n                    if $0 != nil{\n                        print(\"loadFromPreferences\",$0.debugDescription)\n                        complete(nil);return;\n                    }\n                    self.addVPNStatusObserver()\n                    complete(manager)\n                }\n            }\n\n        }\n    }\n\n    func loadProviderManager(_ complete: @escaping (NETunnelProviderManager?) -> Void){\n        NETunnelProviderManager.loadAllFromPreferences { (managers, error) in\n            if let managers = managers {\n                if managers.count > 0 {\n                    let manager = managers[0]\n                    complete(manager)\n                    return\n                }\n            }\n            complete(nil)\n        }\n    }\n\n}\n\n// Actions\nextension VpnManager{\n    \n    func connect(host: String?, port: Int?, ipProxy: Bool? = false, proxyPassDomains: [String]? = nil) {\n        self.proxyHost = host ?? self.proxyHost\n        self.proxyPort = port ?? self.proxyPort\n        self.ipProxy = ipProxy ?? false\n        self.proxyPassDomains = proxyPassDomains ?? self.proxyPassDomains\n\n        self.loadAndCreatePrividerManager { (manager) in\n            guard let manager = manager else{return}\n            do{\n                self.activeVPN = manager\n                try manager.connection.startVPNTunnel()\n            }catch let err{\n                print(\"connect: \", err)\n            }\n        }\n    }\n    \n    func restartConnect(host: String?, port: Int?, ipProxy: Bool? = false, proxyPassDomains: [String]? = nil) {\n        self.proxyHost = host ?? self.proxyHost\n        self.proxyPort = port ?? self.proxyPort\n        self.ipProxy = ipProxy ?? false\n\n        if (activeVPN != nil) {\n            activeVPN?.connection.stopVPNTunnel()\n            activeVPN = nil\n        }\n        \n        self.connect(host: host, port: port, ipProxy: ipProxy, proxyPassDomains: proxyPassDomains)\n    }\n\n    func disconnect() {\n        if (activeVPN != nil) {\n            activeVPN?.connection.stopVPNTunnel()\n            activeVPN = nil\n            return\n        }\n        \n        loadProviderManager{\n            $0?.connection.stopVPNTunnel()\n        }\n    }\n    \n    func isRunning() -> Bool {\n        return vpnStatus == VPNStatus.on\n    }\n\n}\n"
  },
  {
    "path": "ios/Runner/en.lproj/InfoPlist.strings",
    "content": "\"NSCameraUsageDescription\"=\"Scan QR code\";\n\"NSPhotoLibraryUsageDescription\"=\"Access to Photo Library\";\n\"PhotoLibraryAddUsageDescription\"= \"Save image to Photo Library\";\n"
  },
  {
    "path": "ios/Runner/pip/PictureInPictureManager.swift",
    "content": "//\n//  PictureInPicturePlugin.swift\n//  Runner\n//\n//  Created by wanghongen on 2024/1/8.\n//\n\nimport AVKit\nimport UIKit\nimport Flutter\nimport SnapKit\nimport SwiftUI\n\n@available(iOS 13.0.0, *)\nclass PictureInPictureManager: NSObject,AVPictureInPictureControllerDelegate {\n\n    static var shared: PictureInPictureManager!\n    private var channel: FlutterMethodChannel;\n    //播放器\n    private var playerLayer: AVPlayerLayer?\n\n    // 画中画\n    var pipController: AVPictureInPictureController!\n    var pipView: PictureInPictureView?\n    \n    var proxyPort :Int = -1;\n    \n    static func regirst(flutter: FlutterBinaryMessenger) {\n        let channel = FlutterMethodChannel.init(name: \"com.proxy/pictureInPicture\", binaryMessenger: flutter);\n        shared  = PictureInPictureManager(channel: channel)\n    }\n    \n    private init(channel: FlutterMethodChannel) {\n        self.channel = channel\n        super.init()\n\n        channel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in\n//            print(\"画中画 {call.method} methodCallHandler：\\(UIApplication.shared.windows)\")\n            if (\"enterPictureInPictureMode\" == call.method) {\n                let arguments = call.arguments as? Dictionary<String, AnyObject>\n                self.proxyPort = arguments?[\"proxyPort\"] as! Int\n                self.starPiP()\n                result(Bool(true))\n            } else if (\"addData\" == call.method) {\n                self.pipView?.addData(text: call.arguments as! String)\n                \n            }\n        })\n        \n        if AVPictureInPictureController.isPictureInPictureSupported() {\n            do {\n                try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)\n                try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)\n            } catch {\n                print(error)\n            }\n        }\n    }\n    \n    private func initPIP() {\n        if (playerLayer == nil) {\n            setupPlayer()\n        }\n            \n        if (pipController == nil) {\n            print(\"画中画初始化：\\(UIApplication.shared.windows)\")\n            setupPip()\n        }\n    }\n    \n    // 配置播放器\n    private func setupPlayer() {\n        let video = Bundle.main.url(forResource: \"silience\", withExtension: \"mov\")\n        let asset = AVAsset.init(url: video!)\n        let playerItem = AVPlayerItem.init(asset: asset)\n  \n        let player = AVPlayer.init(playerItem: playerItem)\n        \n        playerLayer = AVPlayerLayer(player: player)\n\n        playerLayer?.frame = .init(x: 90, y: 390, width: 180, height: 280)\n        playerLayer?.isHidden = true\n        player.isMuted = true\n        player.allowsExternalPlayback = true\n//        player.play()\n        let view =  UIView()\n        view.layer.addSublayer(playerLayer!)\n\n        UIApplication.shared.keyWindow?.rootViewController?.view.addSubview(view)\n    }\n    \n    // 配置画中画\n    private func setupPip() {\n        pipController = AVPictureInPictureController.init(playerLayer: playerLayer!)!\n        pipController.delegate = self\n//        if #available(iOS 14.2, *) {\n//            pipController.canStartPictureInPictureAutomaticallyFromInline = true\n//        }\n    \n        // 隐藏播放按钮、快进快退按钮\n        pipController.setValue(1, forKey: \"controlsStyle\")\n        //点击回到app\n        //pipController.setValue(2, forKey: \"controlsStyle\")\n    }\n    \n    \n    // 开启/关闭 画中画\n    func starPiP() {\n        self.initPIP();\n        if pipController.isPictureInPictureActive {\n            pipController.stopPictureInPicture()\n        } else {\n            print(\"starPiP \\(pipController.isPictureInPicturePossible)\")\n\n            if (pipController.isPictureInPicturePossible) {\n                pipController.startPictureInPicture()\n                return;\n            }\n            \n            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { [self] in\n                if (self.pipController.isPictureInPicturePossible) {\n                    self.pipController.startPictureInPicture()\n                    return;\n                }\n                \n                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {\n                    self.pipController.startPictureInPicture()\n                }\n            }\n        }\n    }\n    \n    var playButton  = UIButton(type: .custom)\n    \n    func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {\n//        print(\"画中画初始化后：\\(UIApplication.shared.windows)\")\n        \n       // 把自定义view加到画中画上\n        if let window = UIApplication.shared.windows.first {\n            pipView = PictureInPictureView()\n            let vc = UIHostingController(rootView: pipView)\n             \n            let icon = VpnManager.shared.isRunning() ? \"pause.fill\" : \"play.fill\"\n            playButton.setImage(UIImage(systemName: icon), for: .normal)\n            playButton.addTarget(self, action: #selector(vpnAction), for: .touchUpInside)\n\n            vc.view.addSubview(playButton)\n            playButton.snp.makeConstraints{ (make) in\n                make.left.equalTo(15)\n                make.bottom.equalTo(-13)\n            }\n            \n            let clearButton  = UIButton(type: .custom)\n            clearButton.setImage(UIImage(systemName: \"trash.circle\"), for: .normal)\n            clearButton.addTarget(self, action: #selector(cleanAction), for: .touchUpInside)\n\n            vc.view.addSubview(clearButton)\n            clearButton.snp.makeConstraints{ (make) in\n                make.right.equalTo(-13)\n                make.bottom.equalTo(-13)\n            }\n            \n            window.addSubview(vc.view!)\n            // 使用自动布局\n            vc.view?.snp.makeConstraints { (make) -> Void in\n                make.edges.equalToSuperview()\n            }\n            UIApplication.shared.perform(#selector(NSXPCConnection.suspend))\n        }\n    }\n    \n    @objc func cleanAction() {\n        channel.invokeMethod(\"cleanSession\", arguments: nil)\n        pipView?.dataSource.clear()\n    }\n    \n    @objc func vpnAction() {\n        if (VpnManager.shared.isRunning()) {\n            VpnManager.shared.disconnect()\n            playButton.setImage(UIImage(systemName: \"play.fill\"), for: .normal)\n        } else {\n            VpnManager.shared.connect(host: nil, port: proxyPort, ipProxy: nil)\n            playButton.setImage(UIImage(systemName: \"pause.fill\"), for: .normal)\n        }\n//        pipView?.addData(text: \"hello\")\n    }\n    \n    \n    func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {\n//        print(\"pictureInPictureControllerWillStopPictureInPicture：\")\n        channel.invokeMethod(\"exitPictureInPictureMode\", arguments: nil)\n    }\n\n}\n"
  },
  {
    "path": "ios/Runner/pip/PictureInPictureView.swift",
    "content": "//\n//  PictureInPictureView.swift\n//  Runner\n//\n//  Created by wanghongen on 2024/1/9.\n//\n\nimport SwiftUI\n\n@available(iOS 13.0, *)\nclass DataSource: ObservableObject {\n    @Published var list: [String] = []\n    \n    func clear() {\n        list.removeAll()\n    }\n}\n\n@available(iOS 13.0, *)\nstruct PictureInPictureView: View {\n    @ObservedObject var dataSource = DataSource()\n    \n    var body: some View {\n       \n        ScrollView {\n        \n            VStack(alignment: .leading, spacing: 1.3){\n                \n                ForEach((0..<dataSource.list.count).reversed(), id: \\.self) {\n                    Text(dataSource.list[$0])\n                        .font(.system(size: 10))\n                        .lineLimit(2)\n                        \n                    \n                    Divider()\n                        .frame(maxHeight: 1.3)\n                }\n                \n               \n            }\n            .padding(5)\n            \n           \n        }\n        \n    }\n    \n        func addData(text: String) {\n            dataSource.list.append(text);\n        }\n}\n//\n//\n//class PictureInPictureView: UIView {\n//\n//    private lazy var viewLabel: UITextView = {\n//        let label = UITextView()\n//\n//        label.textContainer.lineBreakMode = .byCharWrapping\n//\n//        label.font = UIFont.systemFont(ofSize: 10)\n////        label.text = \"\"\n//        label.isEditable = false;\n//        label.isSelectable = false;\n////        label.font?.setLine\n////        label.lineSpacing = 1.2\n////        label.textContainer.line\n//\n//\n//        return label\n//      }()\n//\n//    override init(frame: CGRect) {\n//       super.init(frame: frame)\n//       setupUI()\n//   }\n//\n//    required init?(coder: NSCoder) {\n//        fatalError(\"init(coder:) has not been implemented\")\n//    }\n//\n//\n//    private func setupUI() {\n////        backgroundColor = .white\n//        addSubview(viewLabel)\n//        viewLabel.snp.makeConstraints { (make) -> Void in\n//            make.edges.equalToSuperview()\n//        }\n//    }\n//\n//    func addData(text: String) {\n//        let str = \"• \" + text + \"\\n\" + (viewLabel.text ?? \"\");\n//        self.viewLabel.text = str;\n//    }\n//}\n"
  },
  {
    "path": "ios/Runner/zh-Hans.lproj/InfoPlist.strings",
    "content": "\"NSCameraUsageDescription\"=\"扫描二维码\";\n\"NSPhotoLibraryUsageDescription\"=\"访问相册\";\n\"PhotoLibraryAddUsageDescription\"= \"保存图片到相册\";\n"
  },
  {
    "path": "ios/Runner/zh-Hans.lproj/LaunchScreen.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/zh-Hans.lproj/Main.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner.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\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t1FBB39B834EBBDA7C793EA99 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239046BD4495108B4DFCCCB4 /* Pods_Runner.framework */; };\n\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n\t\t9B09121B2A5457B3001108B7 /* VpnManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B09121A2A5457B3001108B7 /* VpnManager.swift */; };\n\t\t9B0912222A54593A001108B7 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B0912212A54593A001108B7 /* NetworkExtension.framework */; };\n\t\t9B0912252A54593A001108B7 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0912242A54593A001108B7 /* PacketTunnelProvider.swift */; };\n\t\t9B09122A2A54593A001108B7 /* ProxyPin.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9B0912202A54593A001108B7 /* ProxyPin.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };\n\t\t9B2A10C62B4CA9A6001C443F /* PictureInPictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */; };\n\t\t9B2A10C82B4CBE32001C443F /* silience.mov in Resources */ = {isa = PBXBuildFile; fileRef = 9B2A10C72B4CBE32001C443F /* silience.mov */; };\n\t\t9B5125AA2CAEE3350027996E /* ICMPPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5125A92CAEE3350027996E /* ICMPPacket.swift */; };\n\t\t9B70772D2A5718FB00F184A9 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B70772C2A5718FB00F184A9 /* AudioManager.swift */; };\n\t\t9B7077362A5728B900F184A9 /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 9B7077352A5728B900F184A9 /* silence.mp3 */; };\n\t\t9B90F5802C183CDE007D7A81 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9B90F5822C183CDE007D7A81 /* InfoPlist.strings */; };\n\t\t9BAB4FC02DE75CFE0093BFBA /* GBPing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB4FBC2DE75CFE0093BFBA /* GBPing.m */; };\n\t\t9BAB4FC12DE75CFE0093BFBA /* GBPingSummary.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB4FBE2DE75CFE0093BFBA /* GBPingSummary.m */; };\n\t\t9BAB4FC32DE75D220093BFBA /* GBPingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB4FC22DE75D220093BFBA /* GBPingHelper.swift */; };\n\t\t9BC4B8CC2B4B48710047DBDD /* PictureInPictureManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */; };\n\t\t9BCA28662C9772DD00C2B46C /* ConnectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */; };\n\t\t9BCA286A2C97748100C2B46C /* IP4Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28692C97748100C2B46C /* IP4Header.swift */; };\n\t\t9BCA286D2C977E3800C2B46C /* TCPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA286C2C977E3800C2B46C /* TCPHeader.swift */; };\n\t\t9BCA286F2C977E4C00C2B46C /* TCPPacketFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */; };\n\t\t9BCA28712C987B0C00C2B46C /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */; };\n\t\t9BCA28732C988E9D00C2B46C /* Packet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28722C988E9D00C2B46C /* Packet.swift */; };\n\t\t9BCA28752C988EC400C2B46C /* TransportHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28742C988EC400C2B46C /* TransportHeader.swift */; };\n\t\t9BCA28782C98902900C2B46C /* PacketUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28772C98902900C2B46C /* PacketUtil.swift */; };\n\t\t9BCA287A2C989A7200C2B46C /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28792C989A7200C2B46C /* Connection.swift */; };\n\t\t9BCA287D2C989A9F00C2B46C /* CloseableConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */; };\n\t\t9BCA287F2C989AF300C2B46C /* NWProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA287E2C989AF300C2B46C /* NWProtocol.swift */; };\n\t\t9BCA28812C98A42A00C2B46C /* ClientPacketWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */; };\n\t\t9BCA28832C98AA9000C2B46C /* ProxyVpnService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */; };\n\t\t9BCA28852C98C6B300C2B46C /* QueueFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28842C98C6B300C2B46C /* QueueFactory.swift */; };\n\t\t9BCA288A2C98C82000C2B46C /* SocketIOService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28892C98C82000C2B46C /* SocketIOService.swift */; };\n\t\t9BCA288C2C995B3700C2B46C /* UDPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA288B2C995B3700C2B46C /* UDPHeader.swift */; };\n\t\t9BE87B5C2DEA480000F4FCEF /* MethodHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE87B5B2DEA47FA00F4FCEF /* MethodHandler.swift */; };\n\t\t9BE87B5E2DEA6BAE00F4FCEF /* TLS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE87B5D2DEA6BAB00F4FCEF /* TLS.swift */; };\n\t\tB375908E625E0AED772FA2C0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 97C146E61CF9000F007C117D /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 97C146ED1CF9000F007C117D;\n\t\t\tremoteInfo = Runner;\n\t\t};\n\t\t9B0912282A54593A001108B7 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 97C146E61CF9000F007C117D /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 9B09121F2A54593A001108B7;\n\t\t\tremoteInfo = ProxyPin;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t9705A1C41CF9048500538489 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t9B09122B2A54593B001108B7 /* Embed Foundation Extensions */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 13;\n\t\t\tfiles = (\n\t\t\t\t9B09122A2A54593A001108B7 /* ProxyPin.appex in Embed Foundation Extensions */,\n\t\t\t);\n\t\t\tname = \"Embed Foundation Extensions\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t0B67A4E592FF13260AAFD656 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.debug.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t239046BD4495108B4DFCCCB4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t306514357AC94BE3DDEBC8D8 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.debug.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t3E54CF83D4EE560125987C8A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t72900351EF1A3F028032459A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.release.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t8215052AB7CBF47CD3DAAF69 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.profile.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t9B0912192A545757001108B7 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = \"<group>\"; };\n\t\t9B09121A2A5457B3001108B7 /* VpnManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VpnManager.swift; sourceTree = \"<group>\"; };\n\t\t9B0912202A54593A001108B7 /* ProxyPin.appex */ = {isa = PBXFileReference; explicitFileType = \"wrapper.app-extension\"; includeInIndex = 0; path = ProxyPin.appex; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t9B0912212A54593A001108B7 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };\n\t\t9B0912242A54593A001108B7 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = \"<group>\"; };\n\t\t9B0912262A54593A001108B7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t9B0912272A54593A001108B7 /* ProxyPin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ProxyPin.entitlements; sourceTree = \"<group>\"; };\n\t\t9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureView.swift; sourceTree = \"<group>\"; };\n\t\t9B2A10C72B4CBE32001C443F /* silience.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = silience.mov; sourceTree = \"<group>\"; };\n\t\t9B5125A92CAEE3350027996E /* ICMPPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICMPPacket.swift; sourceTree = \"<group>\"; };\n\t\t9B70772C2A5718FB00F184A9 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = \"<group>\"; };\n\t\t9B7077352A5728B900F184A9 /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = \"<group>\"; };\n\t\t9B90F57C2C183C7E007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/Main.strings\"; sourceTree = \"<group>\"; };\n\t\t9B90F57D2C183C7E007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/LaunchScreen.strings\"; sourceTree = \"<group>\"; };\n\t\t9B90F5812C183CDE007D7A81 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = \"<group>\"; };\n\t\t9B90F5832C183CE0007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/InfoPlist.strings\"; sourceTree = \"<group>\"; };\n\t\t9BAB4FBB2DE75CFE0093BFBA /* GBPing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GBPing.h; sourceTree = \"<group>\"; };\n\t\t9BAB4FBC2DE75CFE0093BFBA /* GBPing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GBPing.m; sourceTree = \"<group>\"; };\n\t\t9BAB4FBD2DE75CFE0093BFBA /* GBPingSummary.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GBPingSummary.h; sourceTree = \"<group>\"; };\n\t\t9BAB4FBE2DE75CFE0093BFBA /* GBPingSummary.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GBPingSummary.m; sourceTree = \"<group>\"; };\n\t\t9BAB4FBF2DE75CFE0093BFBA /* ICMPHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ICMPHeader.h; sourceTree = \"<group>\"; };\n\t\t9BAB4FC22DE75D220093BFBA /* GBPingHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GBPingHelper.swift; sourceTree = \"<group>\"; };\n\t\t9BAB4FC42DE75E9A0093BFBA /* ProxyPin-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"ProxyPin-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureManager.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionHandler.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28692C97748100C2B46C /* IP4Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IP4Header.swift; sourceTree = \"<group>\"; };\n\t\t9BCA286C2C977E3800C2B46C /* TCPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPHeader.swift; sourceTree = \"<group>\"; };\n\t\t9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPPacketFactory.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionManager.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28722C988E9D00C2B46C /* Packet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Packet.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28742C988EC400C2B46C /* TransportHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportHeader.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28772C98902900C2B46C /* PacketUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketUtil.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28792C989A7200C2B46C /* Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = \"<group>\"; };\n\t\t9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseableConnection.swift; sourceTree = \"<group>\"; };\n\t\t9BCA287E2C989AF300C2B46C /* NWProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWProtocol.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientPacketWriter.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyVpnService.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28842C98C6B300C2B46C /* QueueFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueFactory.swift; sourceTree = \"<group>\"; };\n\t\t9BCA28892C98C82000C2B46C /* SocketIOService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketIOService.swift; sourceTree = \"<group>\"; };\n\t\t9BCA288B2C995B3700C2B46C /* UDPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPHeader.swift; sourceTree = \"<group>\"; };\n\t\t9BE87B5B2DEA47FA00F4FCEF /* MethodHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodHandler.swift; sourceTree = \"<group>\"; };\n\t\t9BE87B5D2DEA6BAB00F4FCEF /* TLS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLS.swift; sourceTree = \"<group>\"; };\n\t\tD37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tE328C7F89A365CDC0EAD15C6 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.release.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t2C2BB3BDC059E8FD67F7FF64 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tB375908E625E0AED772FA2C0 /* Pods_RunnerTests.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t1FBB39B834EBBDA7C793EA99 /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t9B09121D2A54593A001108B7 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t9B0912222A54593A001108B7 /* NetworkExtension.framework 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\t28892733E959FF4F4696A049 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t239046BD4495108B4DFCCCB4 /* Pods_Runner.framework */,\n\t\t\t\tD37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */,\n\t\t\t\t9B0912212A54593A001108B7 /* NetworkExtension.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t331C8082294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t331C807B294A618700263BE5 /* RunnerTests.swift */,\n\t\t\t);\n\t\t\tpath = RunnerTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t8A601D60E7BAF3F69F98077D /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0B67A4E592FF13260AAFD656 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t72900351EF1A3F028032459A /* Pods-Runner.release.xcconfig */,\n\t\t\t\t3E54CF83D4EE560125987C8A /* Pods-Runner.profile.xcconfig */,\n\t\t\t\t306514357AC94BE3DDEBC8D8 /* Pods-RunnerTests.debug.xcconfig */,\n\t\t\t\tE328C7F89A365CDC0EAD15C6 /* Pods-RunnerTests.release.xcconfig */,\n\t\t\t\t8215052AB7CBF47CD3DAAF69 /* Pods-RunnerTests.profile.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t9B0912232A54593A001108B7 /* ProxyPin */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t331C8082294A63A400263BE5 /* RunnerTests */,\n\t\t\t\t8A601D60E7BAF3F69F98077D /* Pods */,\n\t\t\t\t28892733E959FF4F4696A049 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */,\n\t\t\t\t9B0912202A54593A001108B7 /* ProxyPin.appex */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9BE87B5A2DEA47DE00F4FCEF /* Handlers */,\n\t\t\t\t9BC4B8D12B4C19ED0047DBDD /* pip */,\n\t\t\t\t9B7077352A5728B900F184A9 /* silence.mp3 */,\n\t\t\t\t9B09121A2A5457B3001108B7 /* VpnManager.swift */,\n\t\t\t\t9B0912192A545757001108B7 /* Runner.entitlements */,\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t\t9B70772C2A5718FB00F184A9 /* AudioManager.swift */,\n\t\t\t\t9B90F5822C183CDE007D7A81 /* InfoPlist.strings */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9B0912232A54593A001108B7 /* ProxyPin */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9BCA28642C97729000C2B46C /* vpn */,\n\t\t\t\t9B0912242A54593A001108B7 /* PacketTunnelProvider.swift */,\n\t\t\t\t9B0912262A54593A001108B7 /* Info.plist */,\n\t\t\t\t9B0912272A54593A001108B7 /* ProxyPin.entitlements */,\n\t\t\t\t9BAB4FC42DE75E9A0093BFBA /* ProxyPin-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = ProxyPin;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9BAB4FB12DE74F570093BFBA /* ping */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9BAB4FC22DE75D220093BFBA /* GBPingHelper.swift */,\n\t\t\t\t9BAB4FBB2DE75CFE0093BFBA /* GBPing.h */,\n\t\t\t\t9BAB4FBC2DE75CFE0093BFBA /* GBPing.m */,\n\t\t\t\t9BAB4FBD2DE75CFE0093BFBA /* GBPingSummary.h */,\n\t\t\t\t9BAB4FBE2DE75CFE0093BFBA /* GBPingSummary.m */,\n\t\t\t\t9BAB4FBF2DE75CFE0093BFBA /* ICMPHeader.h */,\n\t\t\t);\n\t\t\tpath = ping;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9BC4B8D12B4C19ED0047DBDD /* pip */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9B2A10C72B4CBE32001C443F /* silience.mov */,\n\t\t\t\t9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */,\n\t\t\t\t9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */,\n\t\t\t);\n\t\t\tpath = pip;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9BCA28642C97729000C2B46C /* vpn */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9BAB4FB12DE74F570093BFBA /* ping */,\n\t\t\t\t9BCA287B2C989A8700C2B46C /* socket */,\n\t\t\t\t9BCA28762C98901800C2B46C /* utils */,\n\t\t\t\t9BCA28672C97746200C2B46C /* transport */,\n\t\t\t\t9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */,\n\t\t\t\t9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */,\n\t\t\t\t9BCA28792C989A7200C2B46C /* Connection.swift */,\n\t\t\t\t9BCA287E2C989AF300C2B46C /* NWProtocol.swift */,\n\t\t\t\t9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */,\n\t\t\t\t9BCA28842C98C6B300C2B46C /* QueueFactory.swift */,\n\t\t\t);\n\t\t\tpath = vpn;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9BCA28672C97746200C2B46C /* transport */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9BCA28682C97747000C2B46C /* protocol */,\n\t\t\t\t9BCA28722C988E9D00C2B46C /* Packet.swift */,\n\t\t\t);\n\t\t\tpath = transport;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9BCA28682C97747000C2B46C /* protocol */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9BCA28692C97748100C2B46C /* IP4Header.swift */,\n\t\t\t\t9BCA286C2C977E3800C2B46C /* TCPHeader.swift */,\n\t\t\t\t9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */,\n\t\t\t\t9BCA28742C988EC400C2B46C /* TransportHeader.swift */,\n\t\t\t\t9BCA288B2C995B3700C2B46C /* UDPHeader.swift */,\n\t\t\t\t9B5125A92CAEE3350027996E /* ICMPPacket.swift */,\n\t\t\t);\n\t\t\tpath = protocol;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9BCA28762C98901800C2B46C /* utils */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9BE87B5D2DEA6BAB00F4FCEF /* TLS.swift */,\n\t\t\t\t9BCA28772C98902900C2B46C /* PacketUtil.swift */,\n\t\t\t);\n\t\t\tpath = utils;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9BCA287B2C989A8700C2B46C /* socket */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */,\n\t\t\t\t9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */,\n\t\t\t\t9BCA28892C98C82000C2B46C /* SocketIOService.swift */,\n\t\t\t);\n\t\t\tpath = socket;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9BE87B5A2DEA47DE00F4FCEF /* Handlers */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9BE87B5B2DEA47FA00F4FCEF /* MethodHandler.swift */,\n\t\t\t);\n\t\t\tpath = Handlers;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t331C8080294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tE7E8C74F615A57D43D59596C /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t331C807D294A63A400263BE5 /* Sources */,\n\t\t\t\t331C807F294A63A400263BE5 /* Resources */,\n\t\t\t\t2C2BB3BDC059E8FD67F7FF64 /* Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t35A8CB519E229982B14B0197 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t9B09122B2A54593B001108B7 /* Embed Foundation Extensions */,\n\t\t\t\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t\t593E01BCFF86ADFAC59E51D5 /* [CP] Embed Pods Frameworks */,\n\t\t\t\t298BDEFE069E2E1C3876CA2D /* [CP] Copy Pods Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t9B0912292A54593A001108B7 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n\t\t9B09121F2A54593A001108B7 /* ProxyPin */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 9B09122F2A54593B001108B7 /* Build configuration list for PBXNativeTarget \"ProxyPin\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t9B09121C2A54593A001108B7 /* Sources */,\n\t\t\t\t9B09121D2A54593A001108B7 /* Frameworks */,\n\t\t\t\t9B09121E2A54593A001108B7 /* 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 = ProxyPin;\n\t\t\tproductName = ProxyPin;\n\t\t\tproductReference = 9B0912202A54593A001108B7 /* ProxyPin.appex */;\n\t\t\tproductType = \"com.apple.product-type.app-extension\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 1420;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C8080294A63A400263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 97C146ED1CF9000F007C117D;\n\t\t\t\t\t};\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t\t9B09121F2A54593A001108B7 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.2;\n\t\t\t\t\t\tLastSwiftMigration = 1630;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 13.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\t\"zh-Hans\",\n\t\t\t);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t\t331C8080294A63A400263BE5 /* RunnerTests */,\n\t\t\t\t9B09121F2A54593A001108B7 /* ProxyPin */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C807F294A63A400263BE5 /* 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\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t9B2A10C82B4CBE32001C443F /* silience.mov in Resources */,\n\t\t\t\t9B90F5802C183CDE007D7A81 /* InfoPlist.strings in Resources */,\n\t\t\t\t9B7077362A5728B900F184A9 /* silence.mp3 in Resources */,\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t9B09121E2A54593A001108B7 /* 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/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t298BDEFE069E2E1C3876CA2D /* [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-Runner/Pods-Runner-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-Runner/Pods-Runner-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-Runner/Pods-Runner-resources.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t35A8CB519E229982B14B0197 /* [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-Runner-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\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\",\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\\n\";\n\t\t};\n\t\t593E01BCFF86ADFAC59E51D5 /* [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-Runner/Pods-Runner-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-Runner/Pods-Runner-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-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\";\n\t\t};\n\t\tE7E8C74F615A57D43D59596C /* [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-RunnerTests-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/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t331C807D294A63A400263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t9B2A10C62B4CA9A6001C443F /* PictureInPictureView.swift in Sources */,\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t\t9B70772D2A5718FB00F184A9 /* AudioManager.swift in Sources */,\n\t\t\t\t9BE87B5C2DEA480000F4FCEF /* MethodHandler.swift in Sources */,\n\t\t\t\t9B09121B2A5457B3001108B7 /* VpnManager.swift in Sources */,\n\t\t\t\t9BC4B8CC2B4B48710047DBDD /* PictureInPictureManager.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t9B09121C2A54593A001108B7 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t9BCA28832C98AA9000C2B46C /* ProxyVpnService.swift in Sources */,\n\t\t\t\t9BCA287D2C989A9F00C2B46C /* CloseableConnection.swift in Sources */,\n\t\t\t\t9B5125AA2CAEE3350027996E /* ICMPPacket.swift in Sources */,\n\t\t\t\t9BCA28662C9772DD00C2B46C /* ConnectionHandler.swift in Sources */,\n\t\t\t\t9BCA28712C987B0C00C2B46C /* ConnectionManager.swift in Sources */,\n\t\t\t\t9BCA28812C98A42A00C2B46C /* ClientPacketWriter.swift in Sources */,\n\t\t\t\t9B0912252A54593A001108B7 /* PacketTunnelProvider.swift in Sources */,\n\t\t\t\t9BCA286D2C977E3800C2B46C /* TCPHeader.swift in Sources */,\n\t\t\t\t9BCA28782C98902900C2B46C /* PacketUtil.swift in Sources */,\n\t\t\t\t9BCA288C2C995B3700C2B46C /* UDPHeader.swift in Sources */,\n\t\t\t\t9BCA28732C988E9D00C2B46C /* Packet.swift in Sources */,\n\t\t\t\t9BCA288A2C98C82000C2B46C /* SocketIOService.swift in Sources */,\n\t\t\t\t9BAB4FC02DE75CFE0093BFBA /* GBPing.m in Sources */,\n\t\t\t\t9BAB4FC12DE75CFE0093BFBA /* GBPingSummary.m in Sources */,\n\t\t\t\t9BCA28852C98C6B300C2B46C /* QueueFactory.swift in Sources */,\n\t\t\t\t9BCA286A2C97748100C2B46C /* IP4Header.swift in Sources */,\n\t\t\t\t9BCA287A2C989A7200C2B46C /* Connection.swift in Sources */,\n\t\t\t\t9BAB4FC32DE75D220093BFBA /* GBPingHelper.swift in Sources */,\n\t\t\t\t9BCA28752C988EC400C2B46C /* TransportHeader.swift in Sources */,\n\t\t\t\t9BCA287F2C989AF300C2B46C /* NWProtocol.swift in Sources */,\n\t\t\t\t9BCA286F2C977E4C00C2B46C /* TCPPacketFactory.swift in Sources */,\n\t\t\t\t9BE87B5E2DEA6BAE00F4FCEF /* TLS.swift 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\t331C8086294A63A400263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 97C146ED1CF9000F007C117D /* Runner */;\n\t\t\ttargetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;\n\t\t};\n\t\t9B0912292A54593A001108B7 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 9B09121F2A54593A001108B7 /* ProxyPin */;\n\t\t\ttargetProxy = 9B0912282A54593A001108B7 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t97C146FA1CF9000F007C117D /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FB1CF9000F007C117D /* Base */,\n\t\t\t\t9B90F57C2C183C7E007D7A81 /* zh-Hans */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t\t9B90F57D2C183C7E007D7A81 /* zh-Hans */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9B90F5822C183CDE007D7A81 /* InfoPlist.strings */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t9B90F5812C183CDE007D7A81 /* en */,\n\t\t\t\t9B90F5832C183CE0007D7A81 /* zh-Hans */,\n\t\t\t);\n\t\t\tname = InfoPlist.strings;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\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_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\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_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\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\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.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t249021D4217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEVELOPMENT_TEAM = DM3F8VR243;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.developer-tools\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\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\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t331C8088294A63A400263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 306514357AC94BE3DDEBC8D8 /* Pods-RunnerTests.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.networkProxyFlutter.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C8089294A63A400263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = E328C7F89A365CDC0EAD15C6 /* Pods-RunnerTests.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.networkProxyFlutter.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C808A294A63A400263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 8215052AB7CBF47CD3DAAF69 /* Pods-RunnerTests.profile.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.networkProxyFlutter.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* 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_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\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_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\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\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_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.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147041CF9000F007C117D /* 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_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\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_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\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\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.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t97C147061CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEVELOPMENT_TEAM = DM3F8VR243;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.developer-tools\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\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\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\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\t97C147071CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEVELOPMENT_TEAM = DM3F8VR243;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.developer-tools\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\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\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\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\t9B09122C2A54593B001108B7 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = ProxyPin/ProxyPin.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = DM3F8VR243;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = ProxyPin/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = ProxyPin;\n\t\t\t\tINFOPLIST_KEY_NSHumanReadableCopyright = \"\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\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\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.ProxyPin;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"ProxyPin/ProxyPin-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t9B09122D2A54593B001108B7 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = ProxyPin/ProxyPin.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = DM3F8VR243;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = ProxyPin/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = ProxyPin;\n\t\t\t\tINFOPLIST_KEY_NSHumanReadableCopyright = \"\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\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\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.ProxyPin;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"ProxyPin/ProxyPin-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t9B09122E2A54593B001108B7 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = ProxyPin/ProxyPin.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = DM3F8VR243;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = ProxyPin/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = ProxyPin;\n\t\t\t\tINFOPLIST_KEY_NSHumanReadableCopyright = \"\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\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\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.ProxyPin;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"ProxyPin/ProxyPin-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C8088294A63A400263BE5 /* Debug */,\n\t\t\t\t331C8089294A63A400263BE5 /* Release */,\n\t\t\t\t331C808A294A63A400263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t9B09122F2A54593B001108B7 /* Build configuration list for PBXNativeTarget \"ProxyPin\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t9B09122C2A54593B001108B7 /* Debug */,\n\t\t\t\t9B09122D2A54593B001108B7 /* Release */,\n\t\t\t\t9B09122E2A54593B001108B7 /* Profile */,\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 = 97C146E61CF9000F007C117D /* Project object */;\n}\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.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>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "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>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\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 = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.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      customLLDBInitFile = \"$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            parallelizable = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"331C8080294A63A400263BE5\"\n               BuildableName = \"RunnerTests.xctest\"\n               BlueprintName = \"RunnerTests\"\n               ReferencedContainer = \"container:Runner.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      customLLDBInitFile = \"$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      enableGPUValidationMode = \"1\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.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/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.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>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "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>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/RunnerTests/RunnerTests.swift",
    "content": "import Flutter\nimport UIKit\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add code to the Runner application, consider adding tests here.\n    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.\n  }\n\n}\n"
  },
  {
    "path": "l10n.yaml",
    "content": "#synthetic-package: false\narb-dir: lib/l10n\ntemplate-arb-file: app_en.arb\noutput-localization-file: app_localizations.dart\nuntranslated-messages-file: l10n_errors.txt"
  },
  {
    "path": "lib/l10n/app_en.arb",
    "content": "{\n  \"breakpoint\": \"Breakpoint\",\n  \"breakpointRule\": \"Breakpoint Rule\",\n  \"name\": \"Name\",\n  \"requests\": \"Requests\",\n  \"favorites\": \"Favorites\",\n  \"history\": \"History\",\n  \"toolbox\": \"Toolbox\",\n  \"preference\": \"Preferences\",\n  \"feedback\": \"Feedback\",\n  \"about\": \"About\",\n  \"filter\": \"Proxy Filter\",\n  \"script\": \"Script\",\n  \"share\": \"Share\",\n  \"port\": \"Port: \",\n  \"proxy\": \"Proxy\",\n  \"externalProxy\": \"External Proxy\",\n  \"username\": \"Username\",\n  \"password\": \"Password\",\n  \"proxySetting\": \"Proxy Setting\",\n  \"setAs\": \"Set as \",\n  \"systemProxy\": \"System Proxy\",\n  \"enabledHTTP2\": \"Enable HTTP2\",\n  \"serverNotStart\": \"Proxy server not started\",\n  \"download\": \"Download\",\n  \"config\": \"Configuration\",\n  \"version\": \"Version\",\n\n  \"start\": \"Start\",\n  \"stop\": \"Stop\",\n  \"clear\": \"Clear\",\n  \"httpsProxy\": \"HTTPS Proxy\",\n  \"setting\": \"Settings\",\n  \"mobileConnect\": \"Mobile Connect\",\n  \"connectRemote\": \"Connect Remote\",\n  \"remoteDevice\": \"Remote Device\",\n  \"remoteDeviceList\": \"Remote Device List\",\n  \"myQRCode\": \"My QR Code\",\n\n  \"theme\": \"Theme\",\n  \"followSystem\": \"Follow System\",\n  \"themeColor\": \"Theme Color\",\n  \"themeLight\": \"Light\",\n  \"themeDark\": \"Dark\",\n  \"language\": \"Language\",\n  \"autoStartup\": \"Auto Start Recording Traffic\",\n  \"autoStartupDescribe\": \"Automatically start recording traffic when the program starts\",\n\n  \"copied\": \"Copied to clipboard\",\n  \"execute\": \"Execute\",\n  \"cancel\": \"Cancel\",\n  \"close\": \"Close\",\n  \"save\": \"Save\",\n  \"confirm\": \"Confirm\",\n  \"confirmTitle\": \"Confirm operation\",\n  \"confirmContent\": \"Are you sure about this operation?\",\n  \"addSuccess\": \"Successfully added\",\n  \"saveSuccess\": \"Saved successfully\",\n  \"operationSuccess\": \"Operation succeeded\",\n  \"import\": \"Import\",\n  \"importSuccess\": \"Import successful\",\n  \"importFailed\": \"Import failed\",\n  \"export\": \"Export\",\n  \"exportSuccess\": \"Export successful\",\n  \"exportFailed\": \"Export failed\",\n  \"deleteSuccess\": \"Delete successful\",\n  \"send\": \"Send\",\n  \"fail\": \"fail\",\n  \"success\": \"success\",\n  \"emptyData\": \"Empty Data\",\n  \"requestSuccess\": \"Request successful\",\n  \"add\": \"Add\",\n  \"all\": \"All\",\n  \"modify\": \"Modify\",\n  \"responseType\": \"Response Type\",\n  \"request\": \"Request\",\n  \"response\": \"Response\",\n  \"statusCode\": \"Status code\",\n  \"duration\": \"Duration\",\n\n  \"done\": \"Done\",\n  \"type\": \"Type\",\n  \"enable\": \"Enable\",\n  \"example\": \"Example: \",\n  \"responseHeader\": \"Headers\",\n  \"requestHeader\": \"Headers\",\n  \"requestLine\": \"Request Line\",\n  \"requestMethod\": \"Request Method\",\n  \"param\": \"Param\",\n  \"replaceBodyWith\": \"Replace Body With:\",\n  \"redirectTo\": \"Redirect To:\",\n  \"redirect\": \"Redirect\",\n  \"cannotBeEmpty\": \"Cannot be empty\",\n  \"requestRewriteList\": \"Request Rewrite List\",\n  \"requestRewriteRule\": \"Request Rewrite Rule\",\n  \"requestRewriteEnable\": \"Enable Request Rewrite\",\n  \"action\": \"Action\",\n  \"multiple\": \"Multiple\",\n  \"edit\": \"Edit\",\n  \"disabled\": \"Disabled\",\n  \"requestRewriteDeleteConfirm\": \"Delete {size} rule(s)?\",\n  \"useGuide\": \"Use Guide\",\n  \"pleaseEnter\": \"Please Enter\",\n  \"click\": \"Click\",\n  \"replace\": \"Replace\",\n  \"clickEdit\": \"Click Edit\",\n  \"refresh\": \"Refresh\",\n  \"selectFile\": \"Select file\",\n  \"match\": \"Match\",\n  \"value\": \"Value\",\n  \"matchRule\": \"Match Rule\",\n  \"emptyMatchAll\": \"Empty means match all\",\n  \"newBuilt\": \"New\",\n  \"reportServers\": \"Report Servers\",\n  \"addReportServer\": \"Add Report Server\",\n  \"editReportServer\": \"Edit Report Server\",\n  \"serverUrl\": \"Server URL\",\n  \"compression\": \"Compression\",\n  \"compressionNone\": \"None\",\n  \"newFolder\": \"New Folder\",\n  \"enableSelect\": \"Enable Select\",\n  \"disableSelect\": \"Disable Select\",\n  \"deleteSelect\": \"Delete Select\",\n  \"testData\": \"Test Data\",\n  \"noChangesDetected\": \"No changes detected\",\n  \"enterMatchData\": \"Enter the data to be matched\",\n\n  \"modifyRequestHeader\": \"Modify Header\",\n  \"headerName\": \"Header Name\",\n  \"headerValue\": \"Header Value\",\n  \"deleteHeaderConfirm\": \"Do you want to delete the request header?\",\n\n  \"sequence\": \"All Requests\",\n  \"domainList\": \"Domain List\",\n  \"domainWhitelist\": \"Proxy Domain Whitelist\",\n  \"domainBlacklist\": \"Proxy Domain Blacklist\",\n  \"domainFilter\": \"Proxy Domain List\",\n  \"appWhitelist\": \"App Whitelist\",\n  \"appWhitelistDescribe\": \"Only proxy Apps on the whitelist. If the whitelist is enabled, the blacklist will be invalid\",\n  \"appBlacklist\": \"App Blacklist\",\n  \"scanCode\": \"Scan Code Connect\",\n  \"addBlacklist\": \"Add Proxy Blacklist\",\n  \"addWhitelist\": \"Add Proxy Whitelist\",\n  \"deleteWhitelist\": \"Delete Proxy Whitelist\",\n  \"domainListSubtitle\": \"Last Request Time: {time},  Count: {count}\",\n\n  \"selectAction\": \"Select action\",\n  \"copy\": \"Copy\",\n  \"copyHost\": \"Copy Host\",\n  \"copyUrl\": \"Copy URL\",\n  \"copyRawRequest\": \"Copy Raw Request\",\n  \"copyRequestResponse\": \"Copy Request and Response\",\n  \"copyCurl\": \"Copy cURL\",\n  \"copyAsPythonRequests\": \"Copy as Python Requests\",\n  \"delete\": \"Delete\",\n  \"rename\": \"Rename\",\n  \"repeat\": \"Repeat\",\n  \"repeatAllRequests\": \"Repeat All Requests\",\n  \"repeatDomainRequests\": \"Repeat Domain Requests\",\n  \"customRepeat\": \"Custom Repeat\",\n  \"repeatCount\": \"Iterations\",\n  \"repeatInterval\": \"Interval(ms)\",\n  \"repeatDelay\": \"Delay(ms)\",\n  \"scheduleTime\": \"Schedule Time\",\n  \"fixed\": \"fixed\",\n  \"random\": \"random\",\n  \"keepCustomSettings\": \"Keep custom settings\",\n  \"editRequest\": \"Edit and Request\",\n  \"reSendRequest\": \"The request has been resent\",\n  \"viewExport\": \"View Export\",\n  \"timeDesc\": \"Descending by time\",\n  \"timeAsc\": \"Ascending by time\",\n\n  \"search\": \"Search\",\n  \"clearSearch\": \"Clear Search\",\n  \"requestType\": \"Request type\",\n  \"keyword\": \"Keyword\",\n  \"keywordSearchScope\": \"Keyword search scope: \",\n\n  \"favorite\": \"Favorite\",\n  \"deleteFavorite\": \"Delete Favorite\",\n  \"emptyFavorite\": \"Empty Favorite\",\n  \"deleteFavoriteSuccess\": \"Favorite deleted\",\n\n  \"historyRecord\": \"History\",\n  \"historyCacheTime\": \"Cache Time\",\n  \"historyManualSave\": \"Manual Save\",\n  \"historyDay\": \"{day} days\",\n  \"historyForever\": \"Forever\",\n  \"historyRecordTitle\": \"{name} Records {length}\",\n  \"historyEmptyName\": \"Name cannot be empty\",\n  \"historySubtitle\": \"Records {requestLength}  file {size}\",\n  \"historyUnSave\": \"Current record is not saved\",\n  \"historyDeleteConfirm\": \"Do you want to delete this history?\",\n\n  \"requestEdit\": \"Request Editing\",\n  \"encode\": \"Encode\",\n  \"requestBody\": \"Request Body\",\n  \"responseBody\": \"Response Body\",\n  \"requestRewrite\": \"Request Rewrite\",\n  \"newWindow\": \"New Window\",\n  \"httpRequest\": \"HTTP Request\",\n\n  \"enabledHttps\": \"Enable HTTPS Proxy\",\n  \"installRootCa\": \"Install Certificate\",\n  \"installCaLocal\": \"Install Certificate to Local-Machine\",\n  \"downloadRootCa\": \"Download Certificate\",\n  \"downloadRootCaNote\": \"Note: If you set the default browser to other than Safari, click this line to copy and paste the link to Safari browser\",\n  \"generateCA\": \"Generate new root certificate\",\n  \"generateCADescribe\": \"Are you sure you want to generate a new root certificate? If confirmed,\\nYou need to reinstall and trust the new certificate\",\n  \"resetDefaultCA\": \"Reset Default Root Certificate\",\n  \"resetDefaultCADescribe\": \"Are you sure you want to reset to the default root certificate?\\nProxyPin default root certificate is the same for all users.\",\n  \"exportCaP12\": \"Export Root Certificate(.p12)\",\n  \"importCaP12\": \"Import Root Certificate(.p12)\",\n  \"trustCa\": \"Trust Certificate\",\n  \"profileDownload\": \"Profile Download\",\n  \"exportCA\": \"Export Root Certificate\",\n  \"exportPrivateKey\": \"Export Private Key\",\n  \"install\": \"Install\",\n  \"installCaDescribe\": \"Install CA Setting > Profile Download > Install\",\n  \"trustCaDescribe\": \"Trust CA Setting > General > About > Certificate Trust Setting\",\n  \"androidRoot\": \"System Certificate (ROOT Device)\",\n  \"androidRootMagisk\": \"Magisk module: \\nAndroid ROOT devices can be used Magisk ProxyPinCA System Certificate Module, After installing and restarting the phone Check the system certificate to see if there is a ProxyPinCA certificate. If there is, it indicates that the certificate has been successfully installed。\",\n  \"androidRootRename\": \"If the module does not take effect, you can install the system root certificate according to the online tutorial, and name the root certificate {name}\",\n  \"androidRootCADownload\": \"Download System Certificate(.0)\",\n  \"androidUserCA\": \"User Certificate\",\n  \"androidUserCATips\": \"Tips: Android7+ many apps will not trust user certificates\",\n  \"androidUserCAInstall\": \"Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate\",\n  \"androidUserXposed\": \"It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki\",\n  \"configWifiProxy\": \"Configure mobile Wi-Fi proxy\",\n  \"caInstallGuide\": \"Certificate Installation Guide\",\n  \"caAndroidBrowser\": \"Open Google Browser on Android devices：\",\n  \"caIosBrowser\": \"Open Safari on iOS devices：\",\n\n  \"localIP\": \"Local IP \",\n  \"mobileScan\": \"Configure Wi-Fi proxy or Scan with Mobile App\",\n\n  \"decode\": \"Decode\",\n  \"encodeInput\": \"Enter the content to be converted\",\n  \"encodeResult\": \"Conversion Result\",\n  \"encodeFail\": \"Encoding failed\",\n  \"decodeFail\": \"Decoding failed\",\n\n  \"shareUrl\": \"Share Request URL\",\n  \"shareCurl\": \"Share cURL Request\",\n  \"shareRequestResponse\": \"Share Request and Response\",\n  \"captureDetail\": \"Capture Detail\",\n  \"proxyPinSoftware\": \"ProxyPin Open source traffic capture software for all platforms\",\n\n  \"prompt\": \"Prompt\",\n  \"curlSchemeRequest\": \"If the curl format is recognized, should it be converted into an HTTP request?\",\n  \"appExitTips\": \"Press again to exit the program\",\n  \"remoteConnectDisconnect\": \"Check remote connection failed, disconnected\",\n  \"connect\": \"Connect\",\n  \"reconnect\": \"Reconnect\",\n  \"remoteConnected\": \"Connected {os}, traffic will be forwarded to {os}\",\n  \"remoteConnectForward\": \"Remote connection, forwarding requests to other terminals\",\n  \"connectSuccess\": \"Connect successful\",\n  \"connectedRemote\": \"Connected to remote\",\n  \"connected\": \"Connected\",\n  \"notConnected\": \"Not connected\",\n  \"disconnect\": \"Disconnect\",\n  \"ipLayerProxy\": \"IP Layer Proxy(Beta)\",\n  \"ipLayerProxyDesc\": \"IP layer proxy can capture Flutter app requests, currently not very stable, welcome to submit PR\",\n  \"inputAddress\": \"Input Address\",\n  \"syncConfig\": \"Sync configuration\",\n  \"pullConfigFail\": \"Failed to pull configuration, please check the network connection\",\n  \"sync\": \"Sync\",\n  \"invalidQRCode\": \"Unrecognized QR code\",\n  \"remoteConnectFail\": \"Connection failed，Please check if it is allowed on the same LAN and firewall, iOS needs to enable local network permissions\",\n  \"remoteConnectSuccessTips\": \"Your phone needs to enable packet capture in order to capture requests\",\n\n  \"windowMode\": \"Window Mode\",\n  \"windowModeSubTitle\": \"Enabled Packet Capture, Enter the background, Display a small window\",\n  \"pipIcon\": \"Window shortcut icon\",\n  \"pipIconDescribe\": \"Show quick access to small window Icon\",\n  \"headerExpanded\": \"Headers Expanded\",\n  \"headerExpandedSubtitle\": \"Details page Headers is expanded by default\",\n  \"bottomNavigation\": \"Bottom Navigation\",\n  \"bottomNavigationSubtitle\": \"Bottom navigation bar is displayed, effective after restart\",\n  \"memoryCleanup\": \"Memory Cleanup\",\n  \"memoryCleanupSubtitle\": \"Automatically clean up requests on memory limit reached and keep 32 most recent after cleaning\",\n  \"unlimited\": \"Unlimited\",\n  \"custom\": \"Custom\",\n\n  \"externalProxyAuth\": \"Proxy Auth (Optional)\",\n  \"externalProxyServer\": \"Proxy Server\",\n  \"externalProxyConnectFailure\": \"External Proxy Connect failure\",\n  \"externalProxyFailureConfirm\": \"Access to all http will fail due to network connectivity issues，Do you want to continue setting up external proxies。\",\n  \"mobileDisplayPacketCapture\": \"Mobile Display Packet Capture:\",\n  \"proxyPortRepeat\": \"Startup failed, please check the port number {port} is occupied。\",\n  \"reset\": \"Reset\",\n  \"proxyIgnoreDomain\": \"Proxy ignores domain\",\n  \"domainWhitelistDescribe\": \"Only proxy domain names on the whitelist. If the whitelist is enabled, the blacklist will be invalid\",\n  \"domainBlacklistDescribe\": \"Domain names on the blacklist will not be proxied\",\n  \"domain\": \"Host\",\n  \"enableScript\": \"Enable Script\",\n  \"scriptUseDescribe\": \"Use JavaScript to modify requests and responses\",\n  \"scriptEdit\": \"Edit script\",\n  \"scrollEnd\": \"Scroll to End\",\n  \"logger\": \"Log\",\n  \"material3\": \"Material 3 is the latest version of Google’s open-source design system\",\n\n  \"iosVpnBackgroundAudio\": \"After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?\",\n\n  \"markRead\": \"Mark as read\",\n  \"autoRead\": \"Auto read\",\n  \"highlight\": \"Highlight\",\n  \"blue\" : \"Blue\",\n  \"green\" : \"Green\",\n  \"yellow\" : \"Yellow\",\n  \"red\" : \"Red\",\n  \"pink\" : \"Pink\",\n  \"gray\" : \"Gray\",\n  \"underline\" : \"Underline\",\n\n  \"requestBlock\": \"Request Block\",\n\n  \"other\": \"Other\",\n  \"certHashName\": \"CA Hash Name\",\n  \"regExp\": \"RegExp\",\n  \"systemCertName\": \"System Certificate Name\",\n  \"qrCode\": \"QR Code\",\n  \"scanQrCode\": \"Scan QR Code\",\n  \"generateQrCode\": \"Generate\",\n  \"saveImage\": \"Save Image\",\n  \"selectImage\": \"Select Image\",\n  \"inputContent\": \"Input Content\",\n  \"errorCorrectLevel\": \"Error Correct\",\n  \"output\": \"Output\",\n  \"timestamp\": \"Timestamp\",\n  \"convert\": \"Convert\",\n  \"time\": \"DateTime\",\n  \"nowTimestamp\": \"Now timestamp\",\n  \"hosts\": \"Hosts\",\n  \"toAddress\": \"To Address\",\n  \"encrypt\": \"Encrypt\",\n  \"decrypt\": \"Decrypt\",\n  \"cipher\": \"Cipher\",\n\n  \"appUpdateCheckVersion\": \"Check for Updates\",\n  \"appUpdateNotAvailableMsg\": \"Already Using The Latest Version\",\n  \"appUpdateDialogTitle\": \"Update Available\",\n  \"appUpdateUpdateMsg\": \"A new version of ProxyPin is available. Would you like to update now?\",\n  \"appUpdateCurrentVersionLbl\": \"Current Version\",\n  \"appUpdateNewVersionLbl\": \"New Version\",\n  \"appUpdateUpdateNowBtnTxt\": \"Update Now\",\n  \"appUpdateLaterBtnTxt\": \"Later\",\n  \"appUpdateIgnoreBtnTxt\": \"Ignore\",\n\n  \"requestMap\": \"Request Map\",\n  \"requestMapDescribe\": \"Do not request remote services, use local configuration or script for response\",\n\n  \"automatic\": \"Automatic\",\n  \"manual\": \"Manual\",\n  \"certNotInstalled\": \"Certificate not installed\",\n  \"openNewWindow\": \"Open New Window\",\n  \"sponsorDonate\": \"Sponsor / Donate\",\n  \"sponsorSupport\": \"Support ongoing development\",\n  \"sponsorThanks\": \"Thank you for supporting this open-source project by choosing any of the following methods to help its long-term development.\",\n  \"sponsorAfdian\": \"AFDIAN\",\n  \"sponsorBuyMeCoffee\": \"Buy Me a Coffee\",\n\n  \"privacyPolicy\": \"Privacy Policy\",\n  \"privacyContent\": \"This open-source packet capture tool runs entirely on your device. It has no backend server and does not collect, store, or upload any personal data. All captured traffic is processed locally and is only forwarded when you explicitly use remote forwarding. Permissions (e.g., network, storage, and camera for QR codes) are used solely to provide features. You can audit the behavior in the public source code.\",\n\n  \"requestCrypto\": \"Request Crypto\",\n  \"cryptoDecoded\": \"Decoded\",\n  \"cryptoDecodeToggle\": \"Decrypt\",\n  \"optional\": \"Optional\",\n  \"cryptoRuleField\": \"Field Name\",\n\n  \"cryptoIvPrefixLabel\": \"IV Prefix\",\n  \"cryptoIvPrefixTooltip\": \"Use the first N bytes of the response body as IV\",\n\n  \"local\": \"Local\",\n  \"remoteUrl\": \"Remote URL\",\n  \"view\": \"View\"\n}"
  },
  {
    "path": "lib/l10n/app_localizations.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_localizations/flutter_localizations.dart';\nimport 'package:intl/intl.dart' as intl;\n\nimport 'app_localizations_en.dart';\nimport 'app_localizations_zh.dart';\n\n// ignore_for_file: type=lint\n\n/// Callers can lookup localized strings with an instance of AppLocalizations\n/// returned by `AppLocalizations.of(context)`.\n///\n/// Applications need to include `AppLocalizations.delegate()` in their app's\n/// `localizationDelegates` list, and the locales they support in the app's\n/// `supportedLocales` list. For example:\n///\n/// ```dart\n/// import 'l10n/app_localizations.dart';\n///\n/// return MaterialApp(\n///   localizationsDelegates: AppLocalizations.localizationsDelegates,\n///   supportedLocales: AppLocalizations.supportedLocales,\n///   home: MyApplicationHome(),\n/// );\n/// ```\n///\n/// ## Update pubspec.yaml\n///\n/// Please make sure to update your pubspec.yaml to include the following\n/// packages:\n///\n/// ```yaml\n/// dependencies:\n///   # Internationalization support.\n///   flutter_localizations:\n///     sdk: flutter\n///   intl: any # Use the pinned version from flutter_localizations\n///\n///   # Rest of dependencies\n/// ```\n///\n/// ## iOS Applications\n///\n/// iOS applications define key application metadata, including supported\n/// locales, in an Info.plist file that is built into the application bundle.\n/// To configure the locales supported by your app, you’ll need to edit this\n/// file.\n///\n/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.\n/// Then, in the Project Navigator, open the Info.plist file under the Runner\n/// project’s Runner folder.\n///\n/// Next, select the Information Property List item, select Add Item from the\n/// Editor menu, then select Localizations from the pop-up menu.\n///\n/// Select and expand the newly-created Localizations item then, for each\n/// locale your application supports, add a new item and select the locale\n/// you wish to add from the pop-up menu in the Value field. This list should\n/// be consistent with the languages listed in the AppLocalizations.supportedLocales\n/// property.\nabstract class AppLocalizations {\n  AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());\n\n  final String localeName;\n\n  static AppLocalizations? of(BuildContext context) {\n    return Localizations.of<AppLocalizations>(context, AppLocalizations);\n  }\n\n  static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();\n\n  /// A list of this localizations delegate along with the default localizations\n  /// delegates.\n  ///\n  /// Returns a list of localizations delegates containing this delegate along with\n  /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,\n  /// and GlobalWidgetsLocalizations.delegate.\n  ///\n  /// Additional delegates can be added by appending to this list in\n  /// MaterialApp. This list does not have to be used at all if a custom list\n  /// of delegates is preferred or required.\n  static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[\n    delegate,\n    GlobalMaterialLocalizations.delegate,\n    GlobalCupertinoLocalizations.delegate,\n    GlobalWidgetsLocalizations.delegate,\n  ];\n\n  /// A list of this localizations delegate's supported locales.\n  static const List<Locale> supportedLocales = <Locale>[\n    Locale('en'),\n    Locale('zh'),\n    Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant')\n  ];\n\n  /// No description provided for @breakpoint.\n  ///\n  /// In en, this message translates to:\n  /// **'Breakpoint'**\n  String get breakpoint;\n\n  /// No description provided for @breakpointRule.\n  ///\n  /// In en, this message translates to:\n  /// **'Breakpoint Rule'**\n  String get breakpointRule;\n\n  /// No description provided for @name.\n  ///\n  /// In en, this message translates to:\n  /// **'Name'**\n  String get name;\n\n  /// No description provided for @requests.\n  ///\n  /// In en, this message translates to:\n  /// **'Requests'**\n  String get requests;\n\n  /// No description provided for @favorites.\n  ///\n  /// In en, this message translates to:\n  /// **'Favorites'**\n  String get favorites;\n\n  /// No description provided for @history.\n  ///\n  /// In en, this message translates to:\n  /// **'History'**\n  String get history;\n\n  /// No description provided for @toolbox.\n  ///\n  /// In en, this message translates to:\n  /// **'Toolbox'**\n  String get toolbox;\n\n  /// No description provided for @preference.\n  ///\n  /// In en, this message translates to:\n  /// **'Preferences'**\n  String get preference;\n\n  /// No description provided for @feedback.\n  ///\n  /// In en, this message translates to:\n  /// **'Feedback'**\n  String get feedback;\n\n  /// No description provided for @about.\n  ///\n  /// In en, this message translates to:\n  /// **'About'**\n  String get about;\n\n  /// No description provided for @filter.\n  ///\n  /// In en, this message translates to:\n  /// **'Proxy Filter'**\n  String get filter;\n\n  /// No description provided for @script.\n  ///\n  /// In en, this message translates to:\n  /// **'Script'**\n  String get script;\n\n  /// No description provided for @share.\n  ///\n  /// In en, this message translates to:\n  /// **'Share'**\n  String get share;\n\n  /// No description provided for @port.\n  ///\n  /// In en, this message translates to:\n  /// **'Port: '**\n  String get port;\n\n  /// No description provided for @proxy.\n  ///\n  /// In en, this message translates to:\n  /// **'Proxy'**\n  String get proxy;\n\n  /// No description provided for @externalProxy.\n  ///\n  /// In en, this message translates to:\n  /// **'External Proxy'**\n  String get externalProxy;\n\n  /// No description provided for @username.\n  ///\n  /// In en, this message translates to:\n  /// **'Username'**\n  String get username;\n\n  /// No description provided for @password.\n  ///\n  /// In en, this message translates to:\n  /// **'Password'**\n  String get password;\n\n  /// No description provided for @proxySetting.\n  ///\n  /// In en, this message translates to:\n  /// **'Proxy Setting'**\n  String get proxySetting;\n\n  /// No description provided for @setAs.\n  ///\n  /// In en, this message translates to:\n  /// **'Set as '**\n  String get setAs;\n\n  /// No description provided for @systemProxy.\n  ///\n  /// In en, this message translates to:\n  /// **'System Proxy'**\n  String get systemProxy;\n\n  /// No description provided for @enabledHTTP2.\n  ///\n  /// In en, this message translates to:\n  /// **'Enable HTTP2'**\n  String get enabledHTTP2;\n\n  /// No description provided for @serverNotStart.\n  ///\n  /// In en, this message translates to:\n  /// **'Proxy server not started'**\n  String get serverNotStart;\n\n  /// No description provided for @download.\n  ///\n  /// In en, this message translates to:\n  /// **'Download'**\n  String get download;\n\n  /// No description provided for @config.\n  ///\n  /// In en, this message translates to:\n  /// **'Configuration'**\n  String get config;\n\n  /// No description provided for @version.\n  ///\n  /// In en, this message translates to:\n  /// **'Version'**\n  String get version;\n\n  /// No description provided for @start.\n  ///\n  /// In en, this message translates to:\n  /// **'Start'**\n  String get start;\n\n  /// No description provided for @stop.\n  ///\n  /// In en, this message translates to:\n  /// **'Stop'**\n  String get stop;\n\n  /// No description provided for @clear.\n  ///\n  /// In en, this message translates to:\n  /// **'Clear'**\n  String get clear;\n\n  /// No description provided for @httpsProxy.\n  ///\n  /// In en, this message translates to:\n  /// **'HTTPS Proxy'**\n  String get httpsProxy;\n\n  /// No description provided for @setting.\n  ///\n  /// In en, this message translates to:\n  /// **'Settings'**\n  String get setting;\n\n  /// No description provided for @mobileConnect.\n  ///\n  /// In en, this message translates to:\n  /// **'Mobile Connect'**\n  String get mobileConnect;\n\n  /// No description provided for @connectRemote.\n  ///\n  /// In en, this message translates to:\n  /// **'Connect Remote'**\n  String get connectRemote;\n\n  /// No description provided for @remoteDevice.\n  ///\n  /// In en, this message translates to:\n  /// **'Remote Device'**\n  String get remoteDevice;\n\n  /// No description provided for @remoteDeviceList.\n  ///\n  /// In en, this message translates to:\n  /// **'Remote Device List'**\n  String get remoteDeviceList;\n\n  /// No description provided for @myQRCode.\n  ///\n  /// In en, this message translates to:\n  /// **'My QR Code'**\n  String get myQRCode;\n\n  /// No description provided for @theme.\n  ///\n  /// In en, this message translates to:\n  /// **'Theme'**\n  String get theme;\n\n  /// No description provided for @followSystem.\n  ///\n  /// In en, this message translates to:\n  /// **'Follow System'**\n  String get followSystem;\n\n  /// No description provided for @themeColor.\n  ///\n  /// In en, this message translates to:\n  /// **'Theme Color'**\n  String get themeColor;\n\n  /// No description provided for @themeLight.\n  ///\n  /// In en, this message translates to:\n  /// **'Light'**\n  String get themeLight;\n\n  /// No description provided for @themeDark.\n  ///\n  /// In en, this message translates to:\n  /// **'Dark'**\n  String get themeDark;\n\n  /// No description provided for @language.\n  ///\n  /// In en, this message translates to:\n  /// **'Language'**\n  String get language;\n\n  /// No description provided for @autoStartup.\n  ///\n  /// In en, this message translates to:\n  /// **'Auto Start Recording Traffic'**\n  String get autoStartup;\n\n  /// No description provided for @autoStartupDescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Automatically start recording traffic when the program starts'**\n  String get autoStartupDescribe;\n\n  /// No description provided for @copied.\n  ///\n  /// In en, this message translates to:\n  /// **'Copied to clipboard'**\n  String get copied;\n\n  /// No description provided for @execute.\n  ///\n  /// In en, this message translates to:\n  /// **'Execute'**\n  String get execute;\n\n  /// No description provided for @cancel.\n  ///\n  /// In en, this message translates to:\n  /// **'Cancel'**\n  String get cancel;\n\n  /// No description provided for @close.\n  ///\n  /// In en, this message translates to:\n  /// **'Close'**\n  String get close;\n\n  /// No description provided for @save.\n  ///\n  /// In en, this message translates to:\n  /// **'Save'**\n  String get save;\n\n  /// No description provided for @confirm.\n  ///\n  /// In en, this message translates to:\n  /// **'Confirm'**\n  String get confirm;\n\n  /// No description provided for @confirmTitle.\n  ///\n  /// In en, this message translates to:\n  /// **'Confirm operation'**\n  String get confirmTitle;\n\n  /// No description provided for @confirmContent.\n  ///\n  /// In en, this message translates to:\n  /// **'Are you sure about this operation?'**\n  String get confirmContent;\n\n  /// No description provided for @addSuccess.\n  ///\n  /// In en, this message translates to:\n  /// **'Successfully added'**\n  String get addSuccess;\n\n  /// No description provided for @saveSuccess.\n  ///\n  /// In en, this message translates to:\n  /// **'Saved successfully'**\n  String get saveSuccess;\n\n  /// No description provided for @operationSuccess.\n  ///\n  /// In en, this message translates to:\n  /// **'Operation succeeded'**\n  String get operationSuccess;\n\n  /// No description provided for @import.\n  ///\n  /// In en, this message translates to:\n  /// **'Import'**\n  String get import;\n\n  /// No description provided for @importSuccess.\n  ///\n  /// In en, this message translates to:\n  /// **'Import successful'**\n  String get importSuccess;\n\n  /// No description provided for @importFailed.\n  ///\n  /// In en, this message translates to:\n  /// **'Import failed'**\n  String get importFailed;\n\n  /// No description provided for @export.\n  ///\n  /// In en, this message translates to:\n  /// **'Export'**\n  String get export;\n\n  /// No description provided for @exportSuccess.\n  ///\n  /// In en, this message translates to:\n  /// **'Export successful'**\n  String get exportSuccess;\n\n  /// No description provided for @exportFailed.\n  ///\n  /// In en, this message translates to:\n  /// **'Export failed'**\n  String get exportFailed;\n\n  /// No description provided for @deleteSuccess.\n  ///\n  /// In en, this message translates to:\n  /// **'Delete successful'**\n  String get deleteSuccess;\n\n  /// No description provided for @send.\n  ///\n  /// In en, this message translates to:\n  /// **'Send'**\n  String get send;\n\n  /// No description provided for @fail.\n  ///\n  /// In en, this message translates to:\n  /// **'fail'**\n  String get fail;\n\n  /// No description provided for @success.\n  ///\n  /// In en, this message translates to:\n  /// **'success'**\n  String get success;\n\n  /// No description provided for @emptyData.\n  ///\n  /// In en, this message translates to:\n  /// **'Empty Data'**\n  String get emptyData;\n\n  /// No description provided for @requestSuccess.\n  ///\n  /// In en, this message translates to:\n  /// **'Request successful'**\n  String get requestSuccess;\n\n  /// No description provided for @add.\n  ///\n  /// In en, this message translates to:\n  /// **'Add'**\n  String get add;\n\n  /// No description provided for @all.\n  ///\n  /// In en, this message translates to:\n  /// **'All'**\n  String get all;\n\n  /// No description provided for @modify.\n  ///\n  /// In en, this message translates to:\n  /// **'Modify'**\n  String get modify;\n\n  /// No description provided for @responseType.\n  ///\n  /// In en, this message translates to:\n  /// **'Response Type'**\n  String get responseType;\n\n  /// No description provided for @request.\n  ///\n  /// In en, this message translates to:\n  /// **'Request'**\n  String get request;\n\n  /// No description provided for @response.\n  ///\n  /// In en, this message translates to:\n  /// **'Response'**\n  String get response;\n\n  /// No description provided for @statusCode.\n  ///\n  /// In en, this message translates to:\n  /// **'Status code'**\n  String get statusCode;\n\n  /// No description provided for @duration.\n  ///\n  /// In en, this message translates to:\n  /// **'Duration'**\n  String get duration;\n\n  /// No description provided for @done.\n  ///\n  /// In en, this message translates to:\n  /// **'Done'**\n  String get done;\n\n  /// No description provided for @type.\n  ///\n  /// In en, this message translates to:\n  /// **'Type'**\n  String get type;\n\n  /// No description provided for @enable.\n  ///\n  /// In en, this message translates to:\n  /// **'Enable'**\n  String get enable;\n\n  /// No description provided for @example.\n  ///\n  /// In en, this message translates to:\n  /// **'Example: '**\n  String get example;\n\n  /// No description provided for @responseHeader.\n  ///\n  /// In en, this message translates to:\n  /// **'Headers'**\n  String get responseHeader;\n\n  /// No description provided for @requestHeader.\n  ///\n  /// In en, this message translates to:\n  /// **'Headers'**\n  String get requestHeader;\n\n  /// No description provided for @requestLine.\n  ///\n  /// In en, this message translates to:\n  /// **'Request Line'**\n  String get requestLine;\n\n  /// No description provided for @requestMethod.\n  ///\n  /// In en, this message translates to:\n  /// **'Request Method'**\n  String get requestMethod;\n\n  /// No description provided for @param.\n  ///\n  /// In en, this message translates to:\n  /// **'Param'**\n  String get param;\n\n  /// No description provided for @replaceBodyWith.\n  ///\n  /// In en, this message translates to:\n  /// **'Replace Body With:'**\n  String get replaceBodyWith;\n\n  /// No description provided for @redirectTo.\n  ///\n  /// In en, this message translates to:\n  /// **'Redirect To:'**\n  String get redirectTo;\n\n  /// No description provided for @redirect.\n  ///\n  /// In en, this message translates to:\n  /// **'Redirect'**\n  String get redirect;\n\n  /// No description provided for @cannotBeEmpty.\n  ///\n  /// In en, this message translates to:\n  /// **'Cannot be empty'**\n  String get cannotBeEmpty;\n\n  /// No description provided for @requestRewriteList.\n  ///\n  /// In en, this message translates to:\n  /// **'Request Rewrite List'**\n  String get requestRewriteList;\n\n  /// No description provided for @requestRewriteRule.\n  ///\n  /// In en, this message translates to:\n  /// **'Request Rewrite Rule'**\n  String get requestRewriteRule;\n\n  /// No description provided for @requestRewriteEnable.\n  ///\n  /// In en, this message translates to:\n  /// **'Enable Request Rewrite'**\n  String get requestRewriteEnable;\n\n  /// No description provided for @action.\n  ///\n  /// In en, this message translates to:\n  /// **'Action'**\n  String get action;\n\n  /// No description provided for @multiple.\n  ///\n  /// In en, this message translates to:\n  /// **'Multiple'**\n  String get multiple;\n\n  /// No description provided for @edit.\n  ///\n  /// In en, this message translates to:\n  /// **'Edit'**\n  String get edit;\n\n  /// No description provided for @disabled.\n  ///\n  /// In en, this message translates to:\n  /// **'Disabled'**\n  String get disabled;\n\n  /// No description provided for @requestRewriteDeleteConfirm.\n  ///\n  /// In en, this message translates to:\n  /// **'Delete {size} rule(s)?'**\n  String requestRewriteDeleteConfirm(Object size);\n\n  /// No description provided for @useGuide.\n  ///\n  /// In en, this message translates to:\n  /// **'Use Guide'**\n  String get useGuide;\n\n  /// No description provided for @pleaseEnter.\n  ///\n  /// In en, this message translates to:\n  /// **'Please Enter'**\n  String get pleaseEnter;\n\n  /// No description provided for @click.\n  ///\n  /// In en, this message translates to:\n  /// **'Click'**\n  String get click;\n\n  /// No description provided for @replace.\n  ///\n  /// In en, this message translates to:\n  /// **'Replace'**\n  String get replace;\n\n  /// No description provided for @clickEdit.\n  ///\n  /// In en, this message translates to:\n  /// **'Click Edit'**\n  String get clickEdit;\n\n  /// No description provided for @refresh.\n  ///\n  /// In en, this message translates to:\n  /// **'Refresh'**\n  String get refresh;\n\n  /// No description provided for @selectFile.\n  ///\n  /// In en, this message translates to:\n  /// **'Select file'**\n  String get selectFile;\n\n  /// No description provided for @match.\n  ///\n  /// In en, this message translates to:\n  /// **'Match'**\n  String get match;\n\n  /// No description provided for @value.\n  ///\n  /// In en, this message translates to:\n  /// **'Value'**\n  String get value;\n\n  /// No description provided for @matchRule.\n  ///\n  /// In en, this message translates to:\n  /// **'Match Rule'**\n  String get matchRule;\n\n  /// No description provided for @emptyMatchAll.\n  ///\n  /// In en, this message translates to:\n  /// **'Empty means match all'**\n  String get emptyMatchAll;\n\n  /// No description provided for @newBuilt.\n  ///\n  /// In en, this message translates to:\n  /// **'New'**\n  String get newBuilt;\n\n  /// No description provided for @reportServers.\n  ///\n  /// In en, this message translates to:\n  /// **'Report Servers'**\n  String get reportServers;\n\n  /// No description provided for @addReportServer.\n  ///\n  /// In en, this message translates to:\n  /// **'Add Report Server'**\n  String get addReportServer;\n\n  /// No description provided for @editReportServer.\n  ///\n  /// In en, this message translates to:\n  /// **'Edit Report Server'**\n  String get editReportServer;\n\n  /// No description provided for @serverUrl.\n  ///\n  /// In en, this message translates to:\n  /// **'Server URL'**\n  String get serverUrl;\n\n  /// No description provided for @compression.\n  ///\n  /// In en, this message translates to:\n  /// **'Compression'**\n  String get compression;\n\n  /// No description provided for @compressionNone.\n  ///\n  /// In en, this message translates to:\n  /// **'None'**\n  String get compressionNone;\n\n  /// No description provided for @newFolder.\n  ///\n  /// In en, this message translates to:\n  /// **'New Folder'**\n  String get newFolder;\n\n  /// No description provided for @enableSelect.\n  ///\n  /// In en, this message translates to:\n  /// **'Enable Select'**\n  String get enableSelect;\n\n  /// No description provided for @disableSelect.\n  ///\n  /// In en, this message translates to:\n  /// **'Disable Select'**\n  String get disableSelect;\n\n  /// No description provided for @deleteSelect.\n  ///\n  /// In en, this message translates to:\n  /// **'Delete Select'**\n  String get deleteSelect;\n\n  /// No description provided for @testData.\n  ///\n  /// In en, this message translates to:\n  /// **'Test Data'**\n  String get testData;\n\n  /// No description provided for @noChangesDetected.\n  ///\n  /// In en, this message translates to:\n  /// **'No changes detected'**\n  String get noChangesDetected;\n\n  /// No description provided for @enterMatchData.\n  ///\n  /// In en, this message translates to:\n  /// **'Enter the data to be matched'**\n  String get enterMatchData;\n\n  /// No description provided for @modifyRequestHeader.\n  ///\n  /// In en, this message translates to:\n  /// **'Modify Header'**\n  String get modifyRequestHeader;\n\n  /// No description provided for @headerName.\n  ///\n  /// In en, this message translates to:\n  /// **'Header Name'**\n  String get headerName;\n\n  /// No description provided for @headerValue.\n  ///\n  /// In en, this message translates to:\n  /// **'Header Value'**\n  String get headerValue;\n\n  /// No description provided for @deleteHeaderConfirm.\n  ///\n  /// In en, this message translates to:\n  /// **'Do you want to delete the request header?'**\n  String get deleteHeaderConfirm;\n\n  /// No description provided for @sequence.\n  ///\n  /// In en, this message translates to:\n  /// **'All Requests'**\n  String get sequence;\n\n  /// No description provided for @domainList.\n  ///\n  /// In en, this message translates to:\n  /// **'Domain List'**\n  String get domainList;\n\n  /// No description provided for @domainWhitelist.\n  ///\n  /// In en, this message translates to:\n  /// **'Proxy Domain Whitelist'**\n  String get domainWhitelist;\n\n  /// No description provided for @domainBlacklist.\n  ///\n  /// In en, this message translates to:\n  /// **'Proxy Domain Blacklist'**\n  String get domainBlacklist;\n\n  /// No description provided for @domainFilter.\n  ///\n  /// In en, this message translates to:\n  /// **'Proxy Domain List'**\n  String get domainFilter;\n\n  /// No description provided for @appWhitelist.\n  ///\n  /// In en, this message translates to:\n  /// **'App Whitelist'**\n  String get appWhitelist;\n\n  /// No description provided for @appWhitelistDescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Only proxy Apps on the whitelist. If the whitelist is enabled, the blacklist will be invalid'**\n  String get appWhitelistDescribe;\n\n  /// No description provided for @appBlacklist.\n  ///\n  /// In en, this message translates to:\n  /// **'App Blacklist'**\n  String get appBlacklist;\n\n  /// No description provided for @scanCode.\n  ///\n  /// In en, this message translates to:\n  /// **'Scan Code Connect'**\n  String get scanCode;\n\n  /// No description provided for @addBlacklist.\n  ///\n  /// In en, this message translates to:\n  /// **'Add Proxy Blacklist'**\n  String get addBlacklist;\n\n  /// No description provided for @addWhitelist.\n  ///\n  /// In en, this message translates to:\n  /// **'Add Proxy Whitelist'**\n  String get addWhitelist;\n\n  /// No description provided for @deleteWhitelist.\n  ///\n  /// In en, this message translates to:\n  /// **'Delete Proxy Whitelist'**\n  String get deleteWhitelist;\n\n  /// No description provided for @domainListSubtitle.\n  ///\n  /// In en, this message translates to:\n  /// **'Last Request Time: {time},  Count: {count}'**\n  String domainListSubtitle(Object count, Object time);\n\n  /// No description provided for @selectAction.\n  ///\n  /// In en, this message translates to:\n  /// **'Select action'**\n  String get selectAction;\n\n  /// No description provided for @copy.\n  ///\n  /// In en, this message translates to:\n  /// **'Copy'**\n  String get copy;\n\n  /// No description provided for @copyHost.\n  ///\n  /// In en, this message translates to:\n  /// **'Copy Host'**\n  String get copyHost;\n\n  /// No description provided for @copyUrl.\n  ///\n  /// In en, this message translates to:\n  /// **'Copy URL'**\n  String get copyUrl;\n\n  /// No description provided for @copyRawRequest.\n  ///\n  /// In en, this message translates to:\n  /// **'Copy Raw Request'**\n  String get copyRawRequest;\n\n  /// No description provided for @copyRequestResponse.\n  ///\n  /// In en, this message translates to:\n  /// **'Copy Request and Response'**\n  String get copyRequestResponse;\n\n  /// No description provided for @copyCurl.\n  ///\n  /// In en, this message translates to:\n  /// **'Copy cURL'**\n  String get copyCurl;\n\n  /// No description provided for @copyAsPythonRequests.\n  ///\n  /// In en, this message translates to:\n  /// **'Copy as Python Requests'**\n  String get copyAsPythonRequests;\n\n  /// No description provided for @delete.\n  ///\n  /// In en, this message translates to:\n  /// **'Delete'**\n  String get delete;\n\n  /// No description provided for @rename.\n  ///\n  /// In en, this message translates to:\n  /// **'Rename'**\n  String get rename;\n\n  /// No description provided for @repeat.\n  ///\n  /// In en, this message translates to:\n  /// **'Repeat'**\n  String get repeat;\n\n  /// No description provided for @repeatAllRequests.\n  ///\n  /// In en, this message translates to:\n  /// **'Repeat All Requests'**\n  String get repeatAllRequests;\n\n  /// No description provided for @repeatDomainRequests.\n  ///\n  /// In en, this message translates to:\n  /// **'Repeat Domain Requests'**\n  String get repeatDomainRequests;\n\n  /// No description provided for @customRepeat.\n  ///\n  /// In en, this message translates to:\n  /// **'Custom Repeat'**\n  String get customRepeat;\n\n  /// No description provided for @repeatCount.\n  ///\n  /// In en, this message translates to:\n  /// **'Iterations'**\n  String get repeatCount;\n\n  /// No description provided for @repeatInterval.\n  ///\n  /// In en, this message translates to:\n  /// **'Interval(ms)'**\n  String get repeatInterval;\n\n  /// No description provided for @repeatDelay.\n  ///\n  /// In en, this message translates to:\n  /// **'Delay(ms)'**\n  String get repeatDelay;\n\n  /// No description provided for @scheduleTime.\n  ///\n  /// In en, this message translates to:\n  /// **'Schedule Time'**\n  String get scheduleTime;\n\n  /// No description provided for @fixed.\n  ///\n  /// In en, this message translates to:\n  /// **'fixed'**\n  String get fixed;\n\n  /// No description provided for @random.\n  ///\n  /// In en, this message translates to:\n  /// **'random'**\n  String get random;\n\n  /// No description provided for @keepCustomSettings.\n  ///\n  /// In en, this message translates to:\n  /// **'Keep custom settings'**\n  String get keepCustomSettings;\n\n  /// No description provided for @editRequest.\n  ///\n  /// In en, this message translates to:\n  /// **'Edit and Request'**\n  String get editRequest;\n\n  /// No description provided for @reSendRequest.\n  ///\n  /// In en, this message translates to:\n  /// **'The request has been resent'**\n  String get reSendRequest;\n\n  /// No description provided for @viewExport.\n  ///\n  /// In en, this message translates to:\n  /// **'View Export'**\n  String get viewExport;\n\n  /// No description provided for @timeDesc.\n  ///\n  /// In en, this message translates to:\n  /// **'Descending by time'**\n  String get timeDesc;\n\n  /// No description provided for @timeAsc.\n  ///\n  /// In en, this message translates to:\n  /// **'Ascending by time'**\n  String get timeAsc;\n\n  /// No description provided for @search.\n  ///\n  /// In en, this message translates to:\n  /// **'Search'**\n  String get search;\n\n  /// No description provided for @clearSearch.\n  ///\n  /// In en, this message translates to:\n  /// **'Clear Search'**\n  String get clearSearch;\n\n  /// No description provided for @requestType.\n  ///\n  /// In en, this message translates to:\n  /// **'Request type'**\n  String get requestType;\n\n  /// No description provided for @keyword.\n  ///\n  /// In en, this message translates to:\n  /// **'Keyword'**\n  String get keyword;\n\n  /// No description provided for @keywordSearchScope.\n  ///\n  /// In en, this message translates to:\n  /// **'Keyword search scope: '**\n  String get keywordSearchScope;\n\n  /// No description provided for @favorite.\n  ///\n  /// In en, this message translates to:\n  /// **'Favorite'**\n  String get favorite;\n\n  /// No description provided for @deleteFavorite.\n  ///\n  /// In en, this message translates to:\n  /// **'Delete Favorite'**\n  String get deleteFavorite;\n\n  /// No description provided for @emptyFavorite.\n  ///\n  /// In en, this message translates to:\n  /// **'Empty Favorite'**\n  String get emptyFavorite;\n\n  /// No description provided for @deleteFavoriteSuccess.\n  ///\n  /// In en, this message translates to:\n  /// **'Favorite deleted'**\n  String get deleteFavoriteSuccess;\n\n  /// No description provided for @historyRecord.\n  ///\n  /// In en, this message translates to:\n  /// **'History'**\n  String get historyRecord;\n\n  /// No description provided for @historyCacheTime.\n  ///\n  /// In en, this message translates to:\n  /// **'Cache Time'**\n  String get historyCacheTime;\n\n  /// No description provided for @historyManualSave.\n  ///\n  /// In en, this message translates to:\n  /// **'Manual Save'**\n  String get historyManualSave;\n\n  /// No description provided for @historyDay.\n  ///\n  /// In en, this message translates to:\n  /// **'{day} days'**\n  String historyDay(Object day);\n\n  /// No description provided for @historyForever.\n  ///\n  /// In en, this message translates to:\n  /// **'Forever'**\n  String get historyForever;\n\n  /// No description provided for @historyRecordTitle.\n  ///\n  /// In en, this message translates to:\n  /// **'{name} Records {length}'**\n  String historyRecordTitle(Object length, Object name);\n\n  /// No description provided for @historyEmptyName.\n  ///\n  /// In en, this message translates to:\n  /// **'Name cannot be empty'**\n  String get historyEmptyName;\n\n  /// No description provided for @historySubtitle.\n  ///\n  /// In en, this message translates to:\n  /// **'Records {requestLength}  file {size}'**\n  String historySubtitle(Object requestLength, Object size);\n\n  /// No description provided for @historyUnSave.\n  ///\n  /// In en, this message translates to:\n  /// **'Current record is not saved'**\n  String get historyUnSave;\n\n  /// No description provided for @historyDeleteConfirm.\n  ///\n  /// In en, this message translates to:\n  /// **'Do you want to delete this history?'**\n  String get historyDeleteConfirm;\n\n  /// No description provided for @requestEdit.\n  ///\n  /// In en, this message translates to:\n  /// **'Request Editing'**\n  String get requestEdit;\n\n  /// No description provided for @encode.\n  ///\n  /// In en, this message translates to:\n  /// **'Encode'**\n  String get encode;\n\n  /// No description provided for @requestBody.\n  ///\n  /// In en, this message translates to:\n  /// **'Request Body'**\n  String get requestBody;\n\n  /// No description provided for @responseBody.\n  ///\n  /// In en, this message translates to:\n  /// **'Response Body'**\n  String get responseBody;\n\n  /// No description provided for @requestRewrite.\n  ///\n  /// In en, this message translates to:\n  /// **'Request Rewrite'**\n  String get requestRewrite;\n\n  /// No description provided for @newWindow.\n  ///\n  /// In en, this message translates to:\n  /// **'New Window'**\n  String get newWindow;\n\n  /// No description provided for @httpRequest.\n  ///\n  /// In en, this message translates to:\n  /// **'HTTP Request'**\n  String get httpRequest;\n\n  /// No description provided for @enabledHttps.\n  ///\n  /// In en, this message translates to:\n  /// **'Enable HTTPS Proxy'**\n  String get enabledHttps;\n\n  /// No description provided for @installRootCa.\n  ///\n  /// In en, this message translates to:\n  /// **'Install Certificate'**\n  String get installRootCa;\n\n  /// No description provided for @installCaLocal.\n  ///\n  /// In en, this message translates to:\n  /// **'Install Certificate to Local-Machine'**\n  String get installCaLocal;\n\n  /// No description provided for @downloadRootCa.\n  ///\n  /// In en, this message translates to:\n  /// **'Download Certificate'**\n  String get downloadRootCa;\n\n  /// No description provided for @downloadRootCaNote.\n  ///\n  /// In en, this message translates to:\n  /// **'Note: If you set the default browser to other than Safari, click this line to copy and paste the link to Safari browser'**\n  String get downloadRootCaNote;\n\n  /// No description provided for @generateCA.\n  ///\n  /// In en, this message translates to:\n  /// **'Generate new root certificate'**\n  String get generateCA;\n\n  /// No description provided for @generateCADescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Are you sure you want to generate a new root certificate? If confirmed,\\nYou need to reinstall and trust the new certificate'**\n  String get generateCADescribe;\n\n  /// No description provided for @resetDefaultCA.\n  ///\n  /// In en, this message translates to:\n  /// **'Reset Default Root Certificate'**\n  String get resetDefaultCA;\n\n  /// No description provided for @resetDefaultCADescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Are you sure you want to reset to the default root certificate?\\nProxyPin default root certificate is the same for all users.'**\n  String get resetDefaultCADescribe;\n\n  /// No description provided for @exportCaP12.\n  ///\n  /// In en, this message translates to:\n  /// **'Export Root Certificate(.p12)'**\n  String get exportCaP12;\n\n  /// No description provided for @importCaP12.\n  ///\n  /// In en, this message translates to:\n  /// **'Import Root Certificate(.p12)'**\n  String get importCaP12;\n\n  /// No description provided for @trustCa.\n  ///\n  /// In en, this message translates to:\n  /// **'Trust Certificate'**\n  String get trustCa;\n\n  /// No description provided for @profileDownload.\n  ///\n  /// In en, this message translates to:\n  /// **'Profile Download'**\n  String get profileDownload;\n\n  /// No description provided for @exportCA.\n  ///\n  /// In en, this message translates to:\n  /// **'Export Root Certificate'**\n  String get exportCA;\n\n  /// No description provided for @exportPrivateKey.\n  ///\n  /// In en, this message translates to:\n  /// **'Export Private Key'**\n  String get exportPrivateKey;\n\n  /// No description provided for @install.\n  ///\n  /// In en, this message translates to:\n  /// **'Install'**\n  String get install;\n\n  /// No description provided for @installCaDescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Install CA Setting > Profile Download > Install'**\n  String get installCaDescribe;\n\n  /// No description provided for @trustCaDescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Trust CA Setting > General > About > Certificate Trust Setting'**\n  String get trustCaDescribe;\n\n  /// No description provided for @androidRoot.\n  ///\n  /// In en, this message translates to:\n  /// **'System Certificate (ROOT Device)'**\n  String get androidRoot;\n\n  /// No description provided for @androidRootMagisk.\n  ///\n  /// In en, this message translates to:\n  /// **'Magisk module: \\nAndroid ROOT devices can be used Magisk ProxyPinCA System Certificate Module, After installing and restarting the phone Check the system certificate to see if there is a ProxyPinCA certificate. If there is, it indicates that the certificate has been successfully installed。'**\n  String get androidRootMagisk;\n\n  /// No description provided for @androidRootRename.\n  ///\n  /// In en, this message translates to:\n  /// **'If the module does not take effect, you can install the system root certificate according to the online tutorial, and name the root certificate {name}'**\n  String androidRootRename(Object name);\n\n  /// No description provided for @androidRootCADownload.\n  ///\n  /// In en, this message translates to:\n  /// **'Download System Certificate(.0)'**\n  String get androidRootCADownload;\n\n  /// No description provided for @androidUserCA.\n  ///\n  /// In en, this message translates to:\n  /// **'User Certificate'**\n  String get androidUserCA;\n\n  /// No description provided for @androidUserCATips.\n  ///\n  /// In en, this message translates to:\n  /// **'Tips: Android7+ many apps will not trust user certificates'**\n  String get androidUserCATips;\n\n  /// No description provided for @androidUserCAInstall.\n  ///\n  /// In en, this message translates to:\n  /// **'Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate'**\n  String get androidUserCAInstall;\n\n  /// No description provided for @androidUserXposed.\n  ///\n  /// In en, this message translates to:\n  /// **'It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki'**\n  String get androidUserXposed;\n\n  /// No description provided for @configWifiProxy.\n  ///\n  /// In en, this message translates to:\n  /// **'Configure mobile Wi-Fi proxy'**\n  String get configWifiProxy;\n\n  /// No description provided for @caInstallGuide.\n  ///\n  /// In en, this message translates to:\n  /// **'Certificate Installation Guide'**\n  String get caInstallGuide;\n\n  /// No description provided for @caAndroidBrowser.\n  ///\n  /// In en, this message translates to:\n  /// **'Open Google Browser on Android devices：'**\n  String get caAndroidBrowser;\n\n  /// No description provided for @caIosBrowser.\n  ///\n  /// In en, this message translates to:\n  /// **'Open Safari on iOS devices：'**\n  String get caIosBrowser;\n\n  /// No description provided for @localIP.\n  ///\n  /// In en, this message translates to:\n  /// **'Local IP '**\n  String get localIP;\n\n  /// No description provided for @mobileScan.\n  ///\n  /// In en, this message translates to:\n  /// **'Configure Wi-Fi proxy or Scan with Mobile App'**\n  String get mobileScan;\n\n  /// No description provided for @decode.\n  ///\n  /// In en, this message translates to:\n  /// **'Decode'**\n  String get decode;\n\n  /// No description provided for @encodeInput.\n  ///\n  /// In en, this message translates to:\n  /// **'Enter the content to be converted'**\n  String get encodeInput;\n\n  /// No description provided for @encodeResult.\n  ///\n  /// In en, this message translates to:\n  /// **'Conversion Result'**\n  String get encodeResult;\n\n  /// No description provided for @encodeFail.\n  ///\n  /// In en, this message translates to:\n  /// **'Encoding failed'**\n  String get encodeFail;\n\n  /// No description provided for @decodeFail.\n  ///\n  /// In en, this message translates to:\n  /// **'Decoding failed'**\n  String get decodeFail;\n\n  /// No description provided for @shareUrl.\n  ///\n  /// In en, this message translates to:\n  /// **'Share Request URL'**\n  String get shareUrl;\n\n  /// No description provided for @shareCurl.\n  ///\n  /// In en, this message translates to:\n  /// **'Share cURL Request'**\n  String get shareCurl;\n\n  /// No description provided for @shareRequestResponse.\n  ///\n  /// In en, this message translates to:\n  /// **'Share Request and Response'**\n  String get shareRequestResponse;\n\n  /// No description provided for @captureDetail.\n  ///\n  /// In en, this message translates to:\n  /// **'Capture Detail'**\n  String get captureDetail;\n\n  /// No description provided for @proxyPinSoftware.\n  ///\n  /// In en, this message translates to:\n  /// **'ProxyPin Open source traffic capture software for all platforms'**\n  String get proxyPinSoftware;\n\n  /// No description provided for @prompt.\n  ///\n  /// In en, this message translates to:\n  /// **'Prompt'**\n  String get prompt;\n\n  /// No description provided for @curlSchemeRequest.\n  ///\n  /// In en, this message translates to:\n  /// **'If the curl format is recognized, should it be converted into an HTTP request?'**\n  String get curlSchemeRequest;\n\n  /// No description provided for @appExitTips.\n  ///\n  /// In en, this message translates to:\n  /// **'Press again to exit the program'**\n  String get appExitTips;\n\n  /// No description provided for @remoteConnectDisconnect.\n  ///\n  /// In en, this message translates to:\n  /// **'Check remote connection failed, disconnected'**\n  String get remoteConnectDisconnect;\n\n  /// No description provided for @connect.\n  ///\n  /// In en, this message translates to:\n  /// **'Connect'**\n  String get connect;\n\n  /// No description provided for @reconnect.\n  ///\n  /// In en, this message translates to:\n  /// **'Reconnect'**\n  String get reconnect;\n\n  /// No description provided for @remoteConnected.\n  ///\n  /// In en, this message translates to:\n  /// **'Connected {os}, traffic will be forwarded to {os}'**\n  String remoteConnected(Object os);\n\n  /// No description provided for @remoteConnectForward.\n  ///\n  /// In en, this message translates to:\n  /// **'Remote connection, forwarding requests to other terminals'**\n  String get remoteConnectForward;\n\n  /// No description provided for @connectSuccess.\n  ///\n  /// In en, this message translates to:\n  /// **'Connect successful'**\n  String get connectSuccess;\n\n  /// No description provided for @connectedRemote.\n  ///\n  /// In en, this message translates to:\n  /// **'Connected to remote'**\n  String get connectedRemote;\n\n  /// No description provided for @connected.\n  ///\n  /// In en, this message translates to:\n  /// **'Connected'**\n  String get connected;\n\n  /// No description provided for @notConnected.\n  ///\n  /// In en, this message translates to:\n  /// **'Not connected'**\n  String get notConnected;\n\n  /// No description provided for @disconnect.\n  ///\n  /// In en, this message translates to:\n  /// **'Disconnect'**\n  String get disconnect;\n\n  /// No description provided for @ipLayerProxy.\n  ///\n  /// In en, this message translates to:\n  /// **'IP Layer Proxy(Beta)'**\n  String get ipLayerProxy;\n\n  /// No description provided for @ipLayerProxyDesc.\n  ///\n  /// In en, this message translates to:\n  /// **'IP layer proxy can capture Flutter app requests, currently not very stable, welcome to submit PR'**\n  String get ipLayerProxyDesc;\n\n  /// No description provided for @inputAddress.\n  ///\n  /// In en, this message translates to:\n  /// **'Input Address'**\n  String get inputAddress;\n\n  /// No description provided for @syncConfig.\n  ///\n  /// In en, this message translates to:\n  /// **'Sync configuration'**\n  String get syncConfig;\n\n  /// No description provided for @pullConfigFail.\n  ///\n  /// In en, this message translates to:\n  /// **'Failed to pull configuration, please check the network connection'**\n  String get pullConfigFail;\n\n  /// No description provided for @sync.\n  ///\n  /// In en, this message translates to:\n  /// **'Sync'**\n  String get sync;\n\n  /// No description provided for @invalidQRCode.\n  ///\n  /// In en, this message translates to:\n  /// **'Unrecognized QR code'**\n  String get invalidQRCode;\n\n  /// No description provided for @remoteConnectFail.\n  ///\n  /// In en, this message translates to:\n  /// **'Connection failed，Please check if it is allowed on the same LAN and firewall, iOS needs to enable local network permissions'**\n  String get remoteConnectFail;\n\n  /// No description provided for @remoteConnectSuccessTips.\n  ///\n  /// In en, this message translates to:\n  /// **'Your phone needs to enable packet capture in order to capture requests'**\n  String get remoteConnectSuccessTips;\n\n  /// No description provided for @windowMode.\n  ///\n  /// In en, this message translates to:\n  /// **'Window Mode'**\n  String get windowMode;\n\n  /// No description provided for @windowModeSubTitle.\n  ///\n  /// In en, this message translates to:\n  /// **'Enabled Packet Capture, Enter the background, Display a small window'**\n  String get windowModeSubTitle;\n\n  /// No description provided for @pipIcon.\n  ///\n  /// In en, this message translates to:\n  /// **'Window shortcut icon'**\n  String get pipIcon;\n\n  /// No description provided for @pipIconDescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Show quick access to small window Icon'**\n  String get pipIconDescribe;\n\n  /// No description provided for @headerExpanded.\n  ///\n  /// In en, this message translates to:\n  /// **'Headers Expanded'**\n  String get headerExpanded;\n\n  /// No description provided for @headerExpandedSubtitle.\n  ///\n  /// In en, this message translates to:\n  /// **'Details page Headers is expanded by default'**\n  String get headerExpandedSubtitle;\n\n  /// No description provided for @bottomNavigation.\n  ///\n  /// In en, this message translates to:\n  /// **'Bottom Navigation'**\n  String get bottomNavigation;\n\n  /// No description provided for @bottomNavigationSubtitle.\n  ///\n  /// In en, this message translates to:\n  /// **'Bottom navigation bar is displayed, effective after restart'**\n  String get bottomNavigationSubtitle;\n\n  /// No description provided for @memoryCleanup.\n  ///\n  /// In en, this message translates to:\n  /// **'Memory Cleanup'**\n  String get memoryCleanup;\n\n  /// No description provided for @memoryCleanupSubtitle.\n  ///\n  /// In en, this message translates to:\n  /// **'Automatically clean up requests on memory limit reached and keep 32 most recent after cleaning'**\n  String get memoryCleanupSubtitle;\n\n  /// No description provided for @unlimited.\n  ///\n  /// In en, this message translates to:\n  /// **'Unlimited'**\n  String get unlimited;\n\n  /// No description provided for @custom.\n  ///\n  /// In en, this message translates to:\n  /// **'Custom'**\n  String get custom;\n\n  /// No description provided for @externalProxyAuth.\n  ///\n  /// In en, this message translates to:\n  /// **'Proxy Auth (Optional)'**\n  String get externalProxyAuth;\n\n  /// No description provided for @externalProxyServer.\n  ///\n  /// In en, this message translates to:\n  /// **'Proxy Server'**\n  String get externalProxyServer;\n\n  /// No description provided for @externalProxyConnectFailure.\n  ///\n  /// In en, this message translates to:\n  /// **'External Proxy Connect failure'**\n  String get externalProxyConnectFailure;\n\n  /// No description provided for @externalProxyFailureConfirm.\n  ///\n  /// In en, this message translates to:\n  /// **'Access to all http will fail due to network connectivity issues，Do you want to continue setting up external proxies。'**\n  String get externalProxyFailureConfirm;\n\n  /// No description provided for @mobileDisplayPacketCapture.\n  ///\n  /// In en, this message translates to:\n  /// **'Mobile Display Packet Capture:'**\n  String get mobileDisplayPacketCapture;\n\n  /// No description provided for @proxyPortRepeat.\n  ///\n  /// In en, this message translates to:\n  /// **'Startup failed, please check the port number {port} is occupied。'**\n  String proxyPortRepeat(Object port);\n\n  /// No description provided for @reset.\n  ///\n  /// In en, this message translates to:\n  /// **'Reset'**\n  String get reset;\n\n  /// No description provided for @proxyIgnoreDomain.\n  ///\n  /// In en, this message translates to:\n  /// **'Proxy ignores domain'**\n  String get proxyIgnoreDomain;\n\n  /// No description provided for @domainWhitelistDescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Only proxy domain names on the whitelist. If the whitelist is enabled, the blacklist will be invalid'**\n  String get domainWhitelistDescribe;\n\n  /// No description provided for @domainBlacklistDescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Domain names on the blacklist will not be proxied'**\n  String get domainBlacklistDescribe;\n\n  /// No description provided for @domain.\n  ///\n  /// In en, this message translates to:\n  /// **'Host'**\n  String get domain;\n\n  /// No description provided for @enableScript.\n  ///\n  /// In en, this message translates to:\n  /// **'Enable Script'**\n  String get enableScript;\n\n  /// No description provided for @scriptUseDescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Use JavaScript to modify requests and responses'**\n  String get scriptUseDescribe;\n\n  /// No description provided for @scriptEdit.\n  ///\n  /// In en, this message translates to:\n  /// **'Edit script'**\n  String get scriptEdit;\n\n  /// No description provided for @scrollEnd.\n  ///\n  /// In en, this message translates to:\n  /// **'Scroll to End'**\n  String get scrollEnd;\n\n  /// No description provided for @logger.\n  ///\n  /// In en, this message translates to:\n  /// **'Log'**\n  String get logger;\n\n  /// No description provided for @material3.\n  ///\n  /// In en, this message translates to:\n  /// **'Material 3 is the latest version of Google’s open-source design system'**\n  String get material3;\n\n  /// No description provided for @iosVpnBackgroundAudio.\n  ///\n  /// In en, this message translates to:\n  /// **'After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?'**\n  String get iosVpnBackgroundAudio;\n\n  /// No description provided for @markRead.\n  ///\n  /// In en, this message translates to:\n  /// **'Mark as read'**\n  String get markRead;\n\n  /// No description provided for @autoRead.\n  ///\n  /// In en, this message translates to:\n  /// **'Auto read'**\n  String get autoRead;\n\n  /// No description provided for @highlight.\n  ///\n  /// In en, this message translates to:\n  /// **'Highlight'**\n  String get highlight;\n\n  /// No description provided for @blue.\n  ///\n  /// In en, this message translates to:\n  /// **'Blue'**\n  String get blue;\n\n  /// No description provided for @green.\n  ///\n  /// In en, this message translates to:\n  /// **'Green'**\n  String get green;\n\n  /// No description provided for @yellow.\n  ///\n  /// In en, this message translates to:\n  /// **'Yellow'**\n  String get yellow;\n\n  /// No description provided for @red.\n  ///\n  /// In en, this message translates to:\n  /// **'Red'**\n  String get red;\n\n  /// No description provided for @pink.\n  ///\n  /// In en, this message translates to:\n  /// **'Pink'**\n  String get pink;\n\n  /// No description provided for @gray.\n  ///\n  /// In en, this message translates to:\n  /// **'Gray'**\n  String get gray;\n\n  /// No description provided for @underline.\n  ///\n  /// In en, this message translates to:\n  /// **'Underline'**\n  String get underline;\n\n  /// No description provided for @requestBlock.\n  ///\n  /// In en, this message translates to:\n  /// **'Request Block'**\n  String get requestBlock;\n\n  /// No description provided for @other.\n  ///\n  /// In en, this message translates to:\n  /// **'Other'**\n  String get other;\n\n  /// No description provided for @certHashName.\n  ///\n  /// In en, this message translates to:\n  /// **'CA Hash Name'**\n  String get certHashName;\n\n  /// No description provided for @regExp.\n  ///\n  /// In en, this message translates to:\n  /// **'RegExp'**\n  String get regExp;\n\n  /// No description provided for @systemCertName.\n  ///\n  /// In en, this message translates to:\n  /// **'System Certificate Name'**\n  String get systemCertName;\n\n  /// No description provided for @qrCode.\n  ///\n  /// In en, this message translates to:\n  /// **'QR Code'**\n  String get qrCode;\n\n  /// No description provided for @scanQrCode.\n  ///\n  /// In en, this message translates to:\n  /// **'Scan QR Code'**\n  String get scanQrCode;\n\n  /// No description provided for @generateQrCode.\n  ///\n  /// In en, this message translates to:\n  /// **'Generate'**\n  String get generateQrCode;\n\n  /// No description provided for @saveImage.\n  ///\n  /// In en, this message translates to:\n  /// **'Save Image'**\n  String get saveImage;\n\n  /// No description provided for @selectImage.\n  ///\n  /// In en, this message translates to:\n  /// **'Select Image'**\n  String get selectImage;\n\n  /// No description provided for @inputContent.\n  ///\n  /// In en, this message translates to:\n  /// **'Input Content'**\n  String get inputContent;\n\n  /// No description provided for @errorCorrectLevel.\n  ///\n  /// In en, this message translates to:\n  /// **'Error Correct'**\n  String get errorCorrectLevel;\n\n  /// No description provided for @output.\n  ///\n  /// In en, this message translates to:\n  /// **'Output'**\n  String get output;\n\n  /// No description provided for @timestamp.\n  ///\n  /// In en, this message translates to:\n  /// **'Timestamp'**\n  String get timestamp;\n\n  /// No description provided for @convert.\n  ///\n  /// In en, this message translates to:\n  /// **'Convert'**\n  String get convert;\n\n  /// No description provided for @time.\n  ///\n  /// In en, this message translates to:\n  /// **'DateTime'**\n  String get time;\n\n  /// No description provided for @nowTimestamp.\n  ///\n  /// In en, this message translates to:\n  /// **'Now timestamp'**\n  String get nowTimestamp;\n\n  /// No description provided for @hosts.\n  ///\n  /// In en, this message translates to:\n  /// **'Hosts'**\n  String get hosts;\n\n  /// No description provided for @toAddress.\n  ///\n  /// In en, this message translates to:\n  /// **'To Address'**\n  String get toAddress;\n\n  /// No description provided for @encrypt.\n  ///\n  /// In en, this message translates to:\n  /// **'Encrypt'**\n  String get encrypt;\n\n  /// No description provided for @decrypt.\n  ///\n  /// In en, this message translates to:\n  /// **'Decrypt'**\n  String get decrypt;\n\n  /// No description provided for @cipher.\n  ///\n  /// In en, this message translates to:\n  /// **'Cipher'**\n  String get cipher;\n\n  /// No description provided for @appUpdateCheckVersion.\n  ///\n  /// In en, this message translates to:\n  /// **'Check for Updates'**\n  String get appUpdateCheckVersion;\n\n  /// No description provided for @appUpdateNotAvailableMsg.\n  ///\n  /// In en, this message translates to:\n  /// **'Already Using The Latest Version'**\n  String get appUpdateNotAvailableMsg;\n\n  /// No description provided for @appUpdateDialogTitle.\n  ///\n  /// In en, this message translates to:\n  /// **'Update Available'**\n  String get appUpdateDialogTitle;\n\n  /// No description provided for @appUpdateUpdateMsg.\n  ///\n  /// In en, this message translates to:\n  /// **'A new version of ProxyPin is available. Would you like to update now?'**\n  String get appUpdateUpdateMsg;\n\n  /// No description provided for @appUpdateCurrentVersionLbl.\n  ///\n  /// In en, this message translates to:\n  /// **'Current Version'**\n  String get appUpdateCurrentVersionLbl;\n\n  /// No description provided for @appUpdateNewVersionLbl.\n  ///\n  /// In en, this message translates to:\n  /// **'New Version'**\n  String get appUpdateNewVersionLbl;\n\n  /// No description provided for @appUpdateUpdateNowBtnTxt.\n  ///\n  /// In en, this message translates to:\n  /// **'Update Now'**\n  String get appUpdateUpdateNowBtnTxt;\n\n  /// No description provided for @appUpdateLaterBtnTxt.\n  ///\n  /// In en, this message translates to:\n  /// **'Later'**\n  String get appUpdateLaterBtnTxt;\n\n  /// No description provided for @appUpdateIgnoreBtnTxt.\n  ///\n  /// In en, this message translates to:\n  /// **'Ignore'**\n  String get appUpdateIgnoreBtnTxt;\n\n  /// No description provided for @requestMap.\n  ///\n  /// In en, this message translates to:\n  /// **'Request Map'**\n  String get requestMap;\n\n  /// No description provided for @requestMapDescribe.\n  ///\n  /// In en, this message translates to:\n  /// **'Do not request remote services, use local configuration or script for response'**\n  String get requestMapDescribe;\n\n  /// No description provided for @automatic.\n  ///\n  /// In en, this message translates to:\n  /// **'Automatic'**\n  String get automatic;\n\n  /// No description provided for @manual.\n  ///\n  /// In en, this message translates to:\n  /// **'Manual'**\n  String get manual;\n\n  /// No description provided for @certNotInstalled.\n  ///\n  /// In en, this message translates to:\n  /// **'Certificate not installed'**\n  String get certNotInstalled;\n\n  /// No description provided for @openNewWindow.\n  ///\n  /// In en, this message translates to:\n  /// **'Open New Window'**\n  String get openNewWindow;\n\n  /// No description provided for @sponsorDonate.\n  ///\n  /// In en, this message translates to:\n  /// **'Sponsor / Donate'**\n  String get sponsorDonate;\n\n  /// No description provided for @sponsorSupport.\n  ///\n  /// In en, this message translates to:\n  /// **'Support ongoing development'**\n  String get sponsorSupport;\n\n  /// No description provided for @sponsorThanks.\n  ///\n  /// In en, this message translates to:\n  /// **'Thank you for supporting this open-source project by choosing any of the following methods to help its long-term development.'**\n  String get sponsorThanks;\n\n  /// No description provided for @sponsorAfdian.\n  ///\n  /// In en, this message translates to:\n  /// **'AFDIAN'**\n  String get sponsorAfdian;\n\n  /// No description provided for @sponsorBuyMeCoffee.\n  ///\n  /// In en, this message translates to:\n  /// **'Buy Me a Coffee'**\n  String get sponsorBuyMeCoffee;\n\n  /// No description provided for @privacyPolicy.\n  ///\n  /// In en, this message translates to:\n  /// **'Privacy Policy'**\n  String get privacyPolicy;\n\n  /// No description provided for @privacyContent.\n  ///\n  /// In en, this message translates to:\n  /// **'This open-source packet capture tool runs entirely on your device. It has no backend server and does not collect, store, or upload any personal data. All captured traffic is processed locally and is only forwarded when you explicitly use remote forwarding. Permissions (e.g., network, storage, and camera for QR codes) are used solely to provide features. You can audit the behavior in the public source code.'**\n  String get privacyContent;\n\n  /// No description provided for @requestCrypto.\n  ///\n  /// In en, this message translates to:\n  /// **'Request Crypto'**\n  String get requestCrypto;\n\n  /// No description provided for @cryptoDecoded.\n  ///\n  /// In en, this message translates to:\n  /// **'Decoded'**\n  String get cryptoDecoded;\n\n  /// No description provided for @cryptoDecodeToggle.\n  ///\n  /// In en, this message translates to:\n  /// **'Decrypt'**\n  String get cryptoDecodeToggle;\n\n  /// No description provided for @optional.\n  ///\n  /// In en, this message translates to:\n  /// **'Optional'**\n  String get optional;\n\n  /// No description provided for @cryptoRuleField.\n  ///\n  /// In en, this message translates to:\n  /// **'Field Name'**\n  String get cryptoRuleField;\n\n  /// No description provided for @cryptoIvPrefixLabel.\n  ///\n  /// In en, this message translates to:\n  /// **'IV Prefix'**\n  String get cryptoIvPrefixLabel;\n\n  /// No description provided for @cryptoIvPrefixTooltip.\n  ///\n  /// In en, this message translates to:\n  /// **'Use the first N bytes of the response body as IV'**\n  String get cryptoIvPrefixTooltip;\n\n  /// No description provided for @local.\n  ///\n  /// In en, this message translates to:\n  /// **'Local'**\n  String get local;\n\n  /// No description provided for @remoteUrl.\n  ///\n  /// In en, this message translates to:\n  /// **'Remote URL'**\n  String get remoteUrl;\n\n  /// No description provided for @view.\n  ///\n  /// In en, this message translates to:\n  /// **'View'**\n  String get view;\n}\n\nclass _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {\n  const _AppLocalizationsDelegate();\n\n  @override\n  Future<AppLocalizations> load(Locale locale) {\n    return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));\n  }\n\n  @override\n  bool isSupported(Locale locale) => <String>['en', 'zh'].contains(locale.languageCode);\n\n  @override\n  bool shouldReload(_AppLocalizationsDelegate old) => false;\n}\n\nAppLocalizations lookupAppLocalizations(Locale locale) {\n  // Lookup logic when language+script codes are specified.\n  switch (locale.languageCode) {\n    case 'zh':\n      {\n        switch (locale.scriptCode) {\n          case 'Hant':\n            return AppLocalizationsZhHant();\n        }\n        break;\n      }\n  }\n\n  // Lookup logic when only language code is specified.\n  switch (locale.languageCode) {\n    case 'en':\n      return AppLocalizationsEn();\n    case 'zh':\n      return AppLocalizationsZh();\n  }\n\n  throw FlutterError('AppLocalizations.delegate failed to load unsupported locale \"$locale\". This is likely '\n      'an issue with the localizations generation tool. Please file an issue '\n      'on GitHub with a reproducible sample app and the gen-l10n configuration '\n      'that was used.');\n}\n"
  },
  {
    "path": "lib/l10n/app_localizations_en.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'app_localizations.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for English (`en`).\nclass AppLocalizationsEn extends AppLocalizations {\n  AppLocalizationsEn([String locale = 'en']) : super(locale);\n\n  @override\n  String get breakpoint => 'Breakpoint';\n\n  @override\n  String get breakpointRule => 'Breakpoint Rule';\n\n  @override\n  String get name => 'Name';\n\n  @override\n  String get requests => 'Requests';\n\n  @override\n  String get favorites => 'Favorites';\n\n  @override\n  String get history => 'History';\n\n  @override\n  String get toolbox => 'Toolbox';\n\n  @override\n  String get preference => 'Preferences';\n\n  @override\n  String get feedback => 'Feedback';\n\n  @override\n  String get about => 'About';\n\n  @override\n  String get filter => 'Proxy Filter';\n\n  @override\n  String get script => 'Script';\n\n  @override\n  String get share => 'Share';\n\n  @override\n  String get port => 'Port: ';\n\n  @override\n  String get proxy => 'Proxy';\n\n  @override\n  String get externalProxy => 'External Proxy';\n\n  @override\n  String get username => 'Username';\n\n  @override\n  String get password => 'Password';\n\n  @override\n  String get proxySetting => 'Proxy Setting';\n\n  @override\n  String get setAs => 'Set as ';\n\n  @override\n  String get systemProxy => 'System Proxy';\n\n  @override\n  String get enabledHTTP2 => 'Enable HTTP2';\n\n  @override\n  String get serverNotStart => 'Proxy server not started';\n\n  @override\n  String get download => 'Download';\n\n  @override\n  String get config => 'Configuration';\n\n  @override\n  String get version => 'Version';\n\n  @override\n  String get start => 'Start';\n\n  @override\n  String get stop => 'Stop';\n\n  @override\n  String get clear => 'Clear';\n\n  @override\n  String get httpsProxy => 'HTTPS Proxy';\n\n  @override\n  String get setting => 'Settings';\n\n  @override\n  String get mobileConnect => 'Mobile Connect';\n\n  @override\n  String get connectRemote => 'Connect Remote';\n\n  @override\n  String get remoteDevice => 'Remote Device';\n\n  @override\n  String get remoteDeviceList => 'Remote Device List';\n\n  @override\n  String get myQRCode => 'My QR Code';\n\n  @override\n  String get theme => 'Theme';\n\n  @override\n  String get followSystem => 'Follow System';\n\n  @override\n  String get themeColor => 'Theme Color';\n\n  @override\n  String get themeLight => 'Light';\n\n  @override\n  String get themeDark => 'Dark';\n\n  @override\n  String get language => 'Language';\n\n  @override\n  String get autoStartup => 'Auto Start Recording Traffic';\n\n  @override\n  String get autoStartupDescribe => 'Automatically start recording traffic when the program starts';\n\n  @override\n  String get copied => 'Copied to clipboard';\n\n  @override\n  String get execute => 'Execute';\n\n  @override\n  String get cancel => 'Cancel';\n\n  @override\n  String get close => 'Close';\n\n  @override\n  String get save => 'Save';\n\n  @override\n  String get confirm => 'Confirm';\n\n  @override\n  String get confirmTitle => 'Confirm operation';\n\n  @override\n  String get confirmContent => 'Are you sure about this operation?';\n\n  @override\n  String get addSuccess => 'Successfully added';\n\n  @override\n  String get saveSuccess => 'Saved successfully';\n\n  @override\n  String get operationSuccess => 'Operation succeeded';\n\n  @override\n  String get import => 'Import';\n\n  @override\n  String get importSuccess => 'Import successful';\n\n  @override\n  String get importFailed => 'Import failed';\n\n  @override\n  String get export => 'Export';\n\n  @override\n  String get exportSuccess => 'Export successful';\n\n  @override\n  String get exportFailed => 'Export failed';\n\n  @override\n  String get deleteSuccess => 'Delete successful';\n\n  @override\n  String get send => 'Send';\n\n  @override\n  String get fail => 'fail';\n\n  @override\n  String get success => 'success';\n\n  @override\n  String get emptyData => 'Empty Data';\n\n  @override\n  String get requestSuccess => 'Request successful';\n\n  @override\n  String get add => 'Add';\n\n  @override\n  String get all => 'All';\n\n  @override\n  String get modify => 'Modify';\n\n  @override\n  String get responseType => 'Response Type';\n\n  @override\n  String get request => 'Request';\n\n  @override\n  String get response => 'Response';\n\n  @override\n  String get statusCode => 'Status code';\n\n  @override\n  String get duration => 'Duration';\n\n  @override\n  String get done => 'Done';\n\n  @override\n  String get type => 'Type';\n\n  @override\n  String get enable => 'Enable';\n\n  @override\n  String get example => 'Example: ';\n\n  @override\n  String get responseHeader => 'Headers';\n\n  @override\n  String get requestHeader => 'Headers';\n\n  @override\n  String get requestLine => 'Request Line';\n\n  @override\n  String get requestMethod => 'Request Method';\n\n  @override\n  String get param => 'Param';\n\n  @override\n  String get replaceBodyWith => 'Replace Body With:';\n\n  @override\n  String get redirectTo => 'Redirect To:';\n\n  @override\n  String get redirect => 'Redirect';\n\n  @override\n  String get cannotBeEmpty => 'Cannot be empty';\n\n  @override\n  String get requestRewriteList => 'Request Rewrite List';\n\n  @override\n  String get requestRewriteRule => 'Request Rewrite Rule';\n\n  @override\n  String get requestRewriteEnable => 'Enable Request Rewrite';\n\n  @override\n  String get action => 'Action';\n\n  @override\n  String get multiple => 'Multiple';\n\n  @override\n  String get edit => 'Edit';\n\n  @override\n  String get disabled => 'Disabled';\n\n  @override\n  String requestRewriteDeleteConfirm(Object size) {\n    return 'Delete $size rule(s)?';\n  }\n\n  @override\n  String get useGuide => 'Use Guide';\n\n  @override\n  String get pleaseEnter => 'Please Enter';\n\n  @override\n  String get click => 'Click';\n\n  @override\n  String get replace => 'Replace';\n\n  @override\n  String get clickEdit => 'Click Edit';\n\n  @override\n  String get refresh => 'Refresh';\n\n  @override\n  String get selectFile => 'Select file';\n\n  @override\n  String get match => 'Match';\n\n  @override\n  String get value => 'Value';\n\n  @override\n  String get matchRule => 'Match Rule';\n\n  @override\n  String get emptyMatchAll => 'Empty means match all';\n\n  @override\n  String get newBuilt => 'New';\n\n  @override\n  String get reportServers => 'Report Servers';\n\n  @override\n  String get addReportServer => 'Add Report Server';\n\n  @override\n  String get editReportServer => 'Edit Report Server';\n\n  @override\n  String get serverUrl => 'Server URL';\n\n  @override\n  String get compression => 'Compression';\n\n  @override\n  String get compressionNone => 'None';\n\n  @override\n  String get newFolder => 'New Folder';\n\n  @override\n  String get enableSelect => 'Enable Select';\n\n  @override\n  String get disableSelect => 'Disable Select';\n\n  @override\n  String get deleteSelect => 'Delete Select';\n\n  @override\n  String get testData => 'Test Data';\n\n  @override\n  String get noChangesDetected => 'No changes detected';\n\n  @override\n  String get enterMatchData => 'Enter the data to be matched';\n\n  @override\n  String get modifyRequestHeader => 'Modify Header';\n\n  @override\n  String get headerName => 'Header Name';\n\n  @override\n  String get headerValue => 'Header Value';\n\n  @override\n  String get deleteHeaderConfirm => 'Do you want to delete the request header?';\n\n  @override\n  String get sequence => 'All Requests';\n\n  @override\n  String get domainList => 'Domain List';\n\n  @override\n  String get domainWhitelist => 'Proxy Domain Whitelist';\n\n  @override\n  String get domainBlacklist => 'Proxy Domain Blacklist';\n\n  @override\n  String get domainFilter => 'Proxy Domain List';\n\n  @override\n  String get appWhitelist => 'App Whitelist';\n\n  @override\n  String get appWhitelistDescribe =>\n      'Only proxy Apps on the whitelist. If the whitelist is enabled, the blacklist will be invalid';\n\n  @override\n  String get appBlacklist => 'App Blacklist';\n\n  @override\n  String get scanCode => 'Scan Code Connect';\n\n  @override\n  String get addBlacklist => 'Add Proxy Blacklist';\n\n  @override\n  String get addWhitelist => 'Add Proxy Whitelist';\n\n  @override\n  String get deleteWhitelist => 'Delete Proxy Whitelist';\n\n  @override\n  String domainListSubtitle(Object count, Object time) {\n    return 'Last Request Time: $time,  Count: $count';\n  }\n\n  @override\n  String get selectAction => 'Select action';\n\n  @override\n  String get copy => 'Copy';\n\n  @override\n  String get copyHost => 'Copy Host';\n\n  @override\n  String get copyUrl => 'Copy URL';\n\n  @override\n  String get copyRawRequest => 'Copy Raw Request';\n\n  @override\n  String get copyRequestResponse => 'Copy Request and Response';\n\n  @override\n  String get copyCurl => 'Copy cURL';\n\n  @override\n  String get copyAsPythonRequests => 'Copy as Python Requests';\n\n  @override\n  String get delete => 'Delete';\n\n  @override\n  String get rename => 'Rename';\n\n  @override\n  String get repeat => 'Repeat';\n\n  @override\n  String get repeatAllRequests => 'Repeat All Requests';\n\n  @override\n  String get repeatDomainRequests => 'Repeat Domain Requests';\n\n  @override\n  String get customRepeat => 'Custom Repeat';\n\n  @override\n  String get repeatCount => 'Iterations';\n\n  @override\n  String get repeatInterval => 'Interval(ms)';\n\n  @override\n  String get repeatDelay => 'Delay(ms)';\n\n  @override\n  String get scheduleTime => 'Schedule Time';\n\n  @override\n  String get fixed => 'fixed';\n\n  @override\n  String get random => 'random';\n\n  @override\n  String get keepCustomSettings => 'Keep custom settings';\n\n  @override\n  String get editRequest => 'Edit and Request';\n\n  @override\n  String get reSendRequest => 'The request has been resent';\n\n  @override\n  String get viewExport => 'View Export';\n\n  @override\n  String get timeDesc => 'Descending by time';\n\n  @override\n  String get timeAsc => 'Ascending by time';\n\n  @override\n  String get search => 'Search';\n\n  @override\n  String get clearSearch => 'Clear Search';\n\n  @override\n  String get requestType => 'Request type';\n\n  @override\n  String get keyword => 'Keyword';\n\n  @override\n  String get keywordSearchScope => 'Keyword search scope: ';\n\n  @override\n  String get favorite => 'Favorite';\n\n  @override\n  String get deleteFavorite => 'Delete Favorite';\n\n  @override\n  String get emptyFavorite => 'Empty Favorite';\n\n  @override\n  String get deleteFavoriteSuccess => 'Favorite deleted';\n\n  @override\n  String get historyRecord => 'History';\n\n  @override\n  String get historyCacheTime => 'Cache Time';\n\n  @override\n  String get historyManualSave => 'Manual Save';\n\n  @override\n  String historyDay(Object day) {\n    return '$day days';\n  }\n\n  @override\n  String get historyForever => 'Forever';\n\n  @override\n  String historyRecordTitle(Object length, Object name) {\n    return '$name Records $length';\n  }\n\n  @override\n  String get historyEmptyName => 'Name cannot be empty';\n\n  @override\n  String historySubtitle(Object requestLength, Object size) {\n    return 'Records $requestLength  file $size';\n  }\n\n  @override\n  String get historyUnSave => 'Current record is not saved';\n\n  @override\n  String get historyDeleteConfirm => 'Do you want to delete this history?';\n\n  @override\n  String get requestEdit => 'Request Editing';\n\n  @override\n  String get encode => 'Encode';\n\n  @override\n  String get requestBody => 'Request Body';\n\n  @override\n  String get responseBody => 'Response Body';\n\n  @override\n  String get requestRewrite => 'Request Rewrite';\n\n  @override\n  String get newWindow => 'New Window';\n\n  @override\n  String get httpRequest => 'HTTP Request';\n\n  @override\n  String get enabledHttps => 'Enable HTTPS Proxy';\n\n  @override\n  String get installRootCa => 'Install Certificate';\n\n  @override\n  String get installCaLocal => 'Install Certificate to Local-Machine';\n\n  @override\n  String get downloadRootCa => 'Download Certificate';\n\n  @override\n  String get downloadRootCaNote =>\n      'Note: If you set the default browser to other than Safari, click this line to copy and paste the link to Safari browser';\n\n  @override\n  String get generateCA => 'Generate new root certificate';\n\n  @override\n  String get generateCADescribe =>\n      'Are you sure you want to generate a new root certificate? If confirmed,\\nYou need to reinstall and trust the new certificate';\n\n  @override\n  String get resetDefaultCA => 'Reset Default Root Certificate';\n\n  @override\n  String get resetDefaultCADescribe =>\n      'Are you sure you want to reset to the default root certificate?\\nProxyPin default root certificate is the same for all users.';\n\n  @override\n  String get exportCaP12 => 'Export Root Certificate(.p12)';\n\n  @override\n  String get importCaP12 => 'Import Root Certificate(.p12)';\n\n  @override\n  String get trustCa => 'Trust Certificate';\n\n  @override\n  String get profileDownload => 'Profile Download';\n\n  @override\n  String get exportCA => 'Export Root Certificate';\n\n  @override\n  String get exportPrivateKey => 'Export Private Key';\n\n  @override\n  String get install => 'Install';\n\n  @override\n  String get installCaDescribe => 'Install CA Setting > Profile Download > Install';\n\n  @override\n  String get trustCaDescribe => 'Trust CA Setting > General > About > Certificate Trust Setting';\n\n  @override\n  String get androidRoot => 'System Certificate (ROOT Device)';\n\n  @override\n  String get androidRootMagisk =>\n      'Magisk module: \\nAndroid ROOT devices can be used Magisk ProxyPinCA System Certificate Module, After installing and restarting the phone Check the system certificate to see if there is a ProxyPinCA certificate. If there is, it indicates that the certificate has been successfully installed。';\n\n  @override\n  String androidRootRename(Object name) {\n    return 'If the module does not take effect, you can install the system root certificate according to the online tutorial, and name the root certificate $name';\n  }\n\n  @override\n  String get androidRootCADownload => 'Download System Certificate(.0)';\n\n  @override\n  String get androidUserCA => 'User Certificate';\n\n  @override\n  String get androidUserCATips => 'Tips: Android7+ many apps will not trust user certificates';\n\n  @override\n  String get androidUserCAInstall =>\n      'Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate';\n\n  @override\n  String get androidUserXposed =>\n      'It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki';\n\n  @override\n  String get configWifiProxy => 'Configure mobile Wi-Fi proxy';\n\n  @override\n  String get caInstallGuide => 'Certificate Installation Guide';\n\n  @override\n  String get caAndroidBrowser => 'Open Google Browser on Android devices：';\n\n  @override\n  String get caIosBrowser => 'Open Safari on iOS devices：';\n\n  @override\n  String get localIP => 'Local IP ';\n\n  @override\n  String get mobileScan => 'Configure Wi-Fi proxy or Scan with Mobile App';\n\n  @override\n  String get decode => 'Decode';\n\n  @override\n  String get encodeInput => 'Enter the content to be converted';\n\n  @override\n  String get encodeResult => 'Conversion Result';\n\n  @override\n  String get encodeFail => 'Encoding failed';\n\n  @override\n  String get decodeFail => 'Decoding failed';\n\n  @override\n  String get shareUrl => 'Share Request URL';\n\n  @override\n  String get shareCurl => 'Share cURL Request';\n\n  @override\n  String get shareRequestResponse => 'Share Request and Response';\n\n  @override\n  String get captureDetail => 'Capture Detail';\n\n  @override\n  String get proxyPinSoftware => 'ProxyPin Open source traffic capture software for all platforms';\n\n  @override\n  String get prompt => 'Prompt';\n\n  @override\n  String get curlSchemeRequest => 'If the curl format is recognized, should it be converted into an HTTP request?';\n\n  @override\n  String get appExitTips => 'Press again to exit the program';\n\n  @override\n  String get remoteConnectDisconnect => 'Check remote connection failed, disconnected';\n\n  @override\n  String get connect => 'Connect';\n\n  @override\n  String get reconnect => 'Reconnect';\n\n  @override\n  String remoteConnected(Object os) {\n    return 'Connected $os, traffic will be forwarded to $os';\n  }\n\n  @override\n  String get remoteConnectForward => 'Remote connection, forwarding requests to other terminals';\n\n  @override\n  String get connectSuccess => 'Connect successful';\n\n  @override\n  String get connectedRemote => 'Connected to remote';\n\n  @override\n  String get connected => 'Connected';\n\n  @override\n  String get notConnected => 'Not connected';\n\n  @override\n  String get disconnect => 'Disconnect';\n\n  @override\n  String get ipLayerProxy => 'IP Layer Proxy(Beta)';\n\n  @override\n  String get ipLayerProxyDesc =>\n      'IP layer proxy can capture Flutter app requests, currently not very stable, welcome to submit PR';\n\n  @override\n  String get inputAddress => 'Input Address';\n\n  @override\n  String get syncConfig => 'Sync configuration';\n\n  @override\n  String get pullConfigFail => 'Failed to pull configuration, please check the network connection';\n\n  @override\n  String get sync => 'Sync';\n\n  @override\n  String get invalidQRCode => 'Unrecognized QR code';\n\n  @override\n  String get remoteConnectFail =>\n      'Connection failed，Please check if it is allowed on the same LAN and firewall, iOS needs to enable local network permissions';\n\n  @override\n  String get remoteConnectSuccessTips => 'Your phone needs to enable packet capture in order to capture requests';\n\n  @override\n  String get windowMode => 'Window Mode';\n\n  @override\n  String get windowModeSubTitle => 'Enabled Packet Capture, Enter the background, Display a small window';\n\n  @override\n  String get pipIcon => 'Window shortcut icon';\n\n  @override\n  String get pipIconDescribe => 'Show quick access to small window Icon';\n\n  @override\n  String get headerExpanded => 'Headers Expanded';\n\n  @override\n  String get headerExpandedSubtitle => 'Details page Headers is expanded by default';\n\n  @override\n  String get bottomNavigation => 'Bottom Navigation';\n\n  @override\n  String get bottomNavigationSubtitle => 'Bottom navigation bar is displayed, effective after restart';\n\n  @override\n  String get memoryCleanup => 'Memory Cleanup';\n\n  @override\n  String get memoryCleanupSubtitle =>\n      'Automatically clean up requests on memory limit reached and keep 32 most recent after cleaning';\n\n  @override\n  String get unlimited => 'Unlimited';\n\n  @override\n  String get custom => 'Custom';\n\n  @override\n  String get externalProxyAuth => 'Proxy Auth (Optional)';\n\n  @override\n  String get externalProxyServer => 'Proxy Server';\n\n  @override\n  String get externalProxyConnectFailure => 'External Proxy Connect failure';\n\n  @override\n  String get externalProxyFailureConfirm =>\n      'Access to all http will fail due to network connectivity issues，Do you want to continue setting up external proxies。';\n\n  @override\n  String get mobileDisplayPacketCapture => 'Mobile Display Packet Capture:';\n\n  @override\n  String proxyPortRepeat(Object port) {\n    return 'Startup failed, please check the port number $port is occupied。';\n  }\n\n  @override\n  String get reset => 'Reset';\n\n  @override\n  String get proxyIgnoreDomain => 'Proxy ignores domain';\n\n  @override\n  String get domainWhitelistDescribe =>\n      'Only proxy domain names on the whitelist. If the whitelist is enabled, the blacklist will be invalid';\n\n  @override\n  String get domainBlacklistDescribe => 'Domain names on the blacklist will not be proxied';\n\n  @override\n  String get domain => 'Host';\n\n  @override\n  String get enableScript => 'Enable Script';\n\n  @override\n  String get scriptUseDescribe => 'Use JavaScript to modify requests and responses';\n\n  @override\n  String get scriptEdit => 'Edit script';\n\n  @override\n  String get scrollEnd => 'Scroll to End';\n\n  @override\n  String get logger => 'Log';\n\n  @override\n  String get material3 => 'Material 3 is the latest version of Google’s open-source design system';\n\n  @override\n  String get iosVpnBackgroundAudio =>\n      'After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?';\n\n  @override\n  String get markRead => 'Mark as read';\n\n  @override\n  String get autoRead => 'Auto read';\n\n  @override\n  String get highlight => 'Highlight';\n\n  @override\n  String get blue => 'Blue';\n\n  @override\n  String get green => 'Green';\n\n  @override\n  String get yellow => 'Yellow';\n\n  @override\n  String get red => 'Red';\n\n  @override\n  String get pink => 'Pink';\n\n  @override\n  String get gray => 'Gray';\n\n  @override\n  String get underline => 'Underline';\n\n  @override\n  String get requestBlock => 'Request Block';\n\n  @override\n  String get other => 'Other';\n\n  @override\n  String get certHashName => 'CA Hash Name';\n\n  @override\n  String get regExp => 'RegExp';\n\n  @override\n  String get systemCertName => 'System Certificate Name';\n\n  @override\n  String get qrCode => 'QR Code';\n\n  @override\n  String get scanQrCode => 'Scan QR Code';\n\n  @override\n  String get generateQrCode => 'Generate';\n\n  @override\n  String get saveImage => 'Save Image';\n\n  @override\n  String get selectImage => 'Select Image';\n\n  @override\n  String get inputContent => 'Input Content';\n\n  @override\n  String get errorCorrectLevel => 'Error Correct';\n\n  @override\n  String get output => 'Output';\n\n  @override\n  String get timestamp => 'Timestamp';\n\n  @override\n  String get convert => 'Convert';\n\n  @override\n  String get time => 'DateTime';\n\n  @override\n  String get nowTimestamp => 'Now timestamp';\n\n  @override\n  String get hosts => 'Hosts';\n\n  @override\n  String get toAddress => 'To Address';\n\n  @override\n  String get encrypt => 'Encrypt';\n\n  @override\n  String get decrypt => 'Decrypt';\n\n  @override\n  String get cipher => 'Cipher';\n\n  @override\n  String get appUpdateCheckVersion => 'Check for Updates';\n\n  @override\n  String get appUpdateNotAvailableMsg => 'Already Using The Latest Version';\n\n  @override\n  String get appUpdateDialogTitle => 'Update Available';\n\n  @override\n  String get appUpdateUpdateMsg => 'A new version of ProxyPin is available. Would you like to update now?';\n\n  @override\n  String get appUpdateCurrentVersionLbl => 'Current Version';\n\n  @override\n  String get appUpdateNewVersionLbl => 'New Version';\n\n  @override\n  String get appUpdateUpdateNowBtnTxt => 'Update Now';\n\n  @override\n  String get appUpdateLaterBtnTxt => 'Later';\n\n  @override\n  String get appUpdateIgnoreBtnTxt => 'Ignore';\n\n  @override\n  String get requestMap => 'Request Map';\n\n  @override\n  String get requestMapDescribe => 'Do not request remote services, use local configuration or script for response';\n\n  @override\n  String get automatic => 'Automatic';\n\n  @override\n  String get manual => 'Manual';\n\n  @override\n  String get certNotInstalled => 'Certificate not installed';\n\n  @override\n  String get openNewWindow => 'Open New Window';\n\n  @override\n  String get sponsorDonate => 'Sponsor / Donate';\n\n  @override\n  String get sponsorSupport => 'Support ongoing development';\n\n  @override\n  String get sponsorThanks =>\n      'Thank you for supporting this open-source project by choosing any of the following methods to help its long-term development.';\n\n  @override\n  String get sponsorAfdian => 'AFDIAN';\n\n  @override\n  String get sponsorBuyMeCoffee => 'Buy Me a Coffee';\n\n  @override\n  String get privacyPolicy => 'Privacy Policy';\n\n  @override\n  String get privacyContent =>\n      'This open-source packet capture tool runs entirely on your device. It has no backend server and does not collect, store, or upload any personal data. All captured traffic is processed locally and is only forwarded when you explicitly use remote forwarding. Permissions (e.g., network, storage, and camera for QR codes) are used solely to provide features. You can audit the behavior in the public source code.';\n\n  @override\n  String get requestCrypto => 'Request Crypto';\n\n  @override\n  String get cryptoDecoded => 'Decoded';\n\n  @override\n  String get cryptoDecodeToggle => 'Decrypt';\n\n  @override\n  String get optional => 'Optional';\n\n  @override\n  String get cryptoRuleField => 'Field Name';\n\n  @override\n  String get cryptoIvPrefixLabel => 'IV Prefix';\n\n  @override\n  String get cryptoIvPrefixTooltip => 'Use the first N bytes of the response body as IV';\n\n  @override\n  String get local => 'Local';\n\n  @override\n  String get remoteUrl => 'Remote URL';\n\n  @override\n  String get view => 'View';\n}\n"
  },
  {
    "path": "lib/l10n/app_localizations_zh.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'app_localizations.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Chinese (`zh`).\nclass AppLocalizationsZh extends AppLocalizations {\n  AppLocalizationsZh([String locale = 'zh']) : super(locale);\n\n  @override\n  String get breakpoint => '断点';\n\n  @override\n  String get breakpointRule => '断点规则';\n\n  @override\n  String get name => '名称';\n\n  @override\n  String get requests => '抓包';\n\n  @override\n  String get favorites => '收藏';\n\n  @override\n  String get history => '历史';\n\n  @override\n  String get toolbox => '工具箱';\n\n  @override\n  String get preference => '偏好设置';\n\n  @override\n  String get feedback => '反馈';\n\n  @override\n  String get about => '关于';\n\n  @override\n  String get filter => '代理过滤';\n\n  @override\n  String get script => '脚本';\n\n  @override\n  String get share => '分享';\n\n  @override\n  String get port => '端口号: ';\n\n  @override\n  String get proxy => '代理';\n\n  @override\n  String get externalProxy => '外部代理设置';\n\n  @override\n  String get username => '用户名';\n\n  @override\n  String get password => '密码';\n\n  @override\n  String get proxySetting => '代理设置';\n\n  @override\n  String get setAs => '设置为';\n\n  @override\n  String get systemProxy => '系统代理';\n\n  @override\n  String get enabledHTTP2 => '启用HTTP2';\n\n  @override\n  String get serverNotStart => '未开启抓包';\n\n  @override\n  String get download => '下载';\n\n  @override\n  String get config => '配置';\n\n  @override\n  String get version => '版本';\n\n  @override\n  String get start => '开始';\n\n  @override\n  String get stop => '停止';\n\n  @override\n  String get clear => '清空';\n\n  @override\n  String get httpsProxy => 'HTTPS 代理';\n\n  @override\n  String get setting => '设置';\n\n  @override\n  String get mobileConnect => '手机连接';\n\n  @override\n  String get connectRemote => '连接终端';\n\n  @override\n  String get remoteDevice => '远程设备';\n\n  @override\n  String get remoteDeviceList => '远程设备列表';\n\n  @override\n  String get myQRCode => '我的二维码';\n\n  @override\n  String get theme => '主题';\n\n  @override\n  String get followSystem => '跟随系统';\n\n  @override\n  String get themeColor => '主题颜色';\n\n  @override\n  String get themeLight => '浅色';\n\n  @override\n  String get themeDark => '深色';\n\n  @override\n  String get language => '语言';\n\n  @override\n  String get autoStartup => '自动开启抓包';\n\n  @override\n  String get autoStartupDescribe => '程序启动时自动开始记录流量';\n\n  @override\n  String get copied => '已复制到剪切板';\n\n  @override\n  String get execute => '执行';\n\n  @override\n  String get cancel => '取消';\n\n  @override\n  String get close => '关闭';\n\n  @override\n  String get save => '保存';\n\n  @override\n  String get confirm => '确认';\n\n  @override\n  String get confirmTitle => '确认操作';\n\n  @override\n  String get confirmContent => '是否确认此操作?';\n\n  @override\n  String get addSuccess => '添加成功';\n\n  @override\n  String get saveSuccess => '保存成功';\n\n  @override\n  String get operationSuccess => '操作成功';\n\n  @override\n  String get import => '导入';\n\n  @override\n  String get importSuccess => '导入成功';\n\n  @override\n  String get importFailed => '导入失败';\n\n  @override\n  String get export => '导出';\n\n  @override\n  String get exportSuccess => '导出成功';\n\n  @override\n  String get exportFailed => '导出失败';\n\n  @override\n  String get deleteSuccess => '删除成功';\n\n  @override\n  String get send => '发送';\n\n  @override\n  String get fail => '失败';\n\n  @override\n  String get success => '成功';\n\n  @override\n  String get emptyData => '无数据';\n\n  @override\n  String get requestSuccess => '请求成功';\n\n  @override\n  String get add => '添加';\n\n  @override\n  String get all => '全部';\n\n  @override\n  String get modify => '修改';\n\n  @override\n  String get responseType => '响应类型';\n\n  @override\n  String get request => '请求';\n\n  @override\n  String get response => '响应';\n\n  @override\n  String get statusCode => '状态码';\n\n  @override\n  String get duration => '耗时';\n\n  @override\n  String get done => '完成';\n\n  @override\n  String get type => '类型';\n\n  @override\n  String get enable => '启用';\n\n  @override\n  String get example => '示例: ';\n\n  @override\n  String get responseHeader => '响应头';\n\n  @override\n  String get requestHeader => '请求头';\n\n  @override\n  String get requestLine => '请求行';\n\n  @override\n  String get requestMethod => '请求方法';\n\n  @override\n  String get param => '参数';\n\n  @override\n  String get replaceBodyWith => '消息体替换为:';\n\n  @override\n  String get redirectTo => '重定向到:';\n\n  @override\n  String get redirect => '重定向';\n\n  @override\n  String get cannotBeEmpty => '不能为空';\n\n  @override\n  String get requestRewriteList => '请求重写列表';\n\n  @override\n  String get requestRewriteRule => '请求重写规则';\n\n  @override\n  String get requestRewriteEnable => '是否启用请求重写';\n\n  @override\n  String get action => '行为';\n\n  @override\n  String get multiple => '多选';\n\n  @override\n  String get edit => '编辑';\n\n  @override\n  String get disabled => '禁用';\n\n  @override\n  String requestRewriteDeleteConfirm(Object size) {\n    return '是否删除$size条规则?';\n  }\n\n  @override\n  String get useGuide => '使用文档';\n\n  @override\n  String get pleaseEnter => '请输入';\n\n  @override\n  String get click => '点击';\n\n  @override\n  String get replace => '替换';\n\n  @override\n  String get clickEdit => '点击编辑';\n\n  @override\n  String get refresh => '刷新';\n\n  @override\n  String get selectFile => '选择文件';\n\n  @override\n  String get match => '匹配';\n\n  @override\n  String get value => '值';\n\n  @override\n  String get matchRule => '匹配规则';\n\n  @override\n  String get emptyMatchAll => '为空表示匹配全部';\n\n  @override\n  String get newBuilt => '新建';\n\n  @override\n  String get reportServers => '上报服务器';\n\n  @override\n  String get addReportServer => '新增上报服务器';\n\n  @override\n  String get editReportServer => '编辑上报服务器';\n\n  @override\n  String get serverUrl => '服务器 URL';\n\n  @override\n  String get compression => '压缩';\n\n  @override\n  String get compressionNone => '无';\n\n  @override\n  String get newFolder => '新建文件夹';\n\n  @override\n  String get enableSelect => '启用选择';\n\n  @override\n  String get disableSelect => '禁用选择';\n\n  @override\n  String get deleteSelect => '删除选择';\n\n  @override\n  String get testData => '测试数据';\n\n  @override\n  String get noChangesDetected => '未检测到变更';\n\n  @override\n  String get enterMatchData => '输入待匹配的数据';\n\n  @override\n  String get modifyRequestHeader => '修改请求头';\n\n  @override\n  String get headerName => '请求头名称';\n\n  @override\n  String get headerValue => '请求头值';\n\n  @override\n  String get deleteHeaderConfirm => '是否删除该请求头';\n\n  @override\n  String get sequence => '全部请求';\n\n  @override\n  String get domainList => '域名列表';\n\n  @override\n  String get domainWhitelist => '代理域名白名单';\n\n  @override\n  String get domainBlacklist => '代理域名黑名单';\n\n  @override\n  String get domainFilter => '域名代理列表';\n\n  @override\n  String get appWhitelist => '应用白名单';\n\n  @override\n  String get appWhitelistDescribe => '只代理白名单中的应用, 白名单启用黑名单将会失效';\n\n  @override\n  String get appBlacklist => '应用黑名单';\n\n  @override\n  String get scanCode => '扫码连接';\n\n  @override\n  String get addBlacklist => '添加代理黑名单';\n\n  @override\n  String get addWhitelist => '添加代理白名单';\n\n  @override\n  String get deleteWhitelist => '删除代理白名单';\n\n  @override\n  String domainListSubtitle(Object count, Object time) {\n    return '最后请求时间: $time,  次数: $count';\n  }\n\n  @override\n  String get selectAction => '选择操作';\n\n  @override\n  String get copy => '复制';\n\n  @override\n  String get copyHost => '复制域名';\n\n  @override\n  String get copyUrl => '复制URL';\n\n  @override\n  String get copyRawRequest => '复制 原始请求';\n\n  @override\n  String get copyRequestResponse => '复制 请求和响应';\n\n  @override\n  String get copyCurl => '复制 cURL';\n\n  @override\n  String get copyAsPythonRequests => '复制 Python Requests';\n\n  @override\n  String get delete => '删除';\n\n  @override\n  String get rename => '重命名';\n\n  @override\n  String get repeat => '重放';\n\n  @override\n  String get repeatAllRequests => '重放所有请求';\n\n  @override\n  String get repeatDomainRequests => '重放域名下请求';\n\n  @override\n  String get customRepeat => '高级重放';\n\n  @override\n  String get repeatCount => '次数';\n\n  @override\n  String get repeatInterval => '间隔(ms)';\n\n  @override\n  String get repeatDelay => '延时(ms)';\n\n  @override\n  String get scheduleTime => '指定时间';\n\n  @override\n  String get fixed => '固定';\n\n  @override\n  String get random => '随机';\n\n  @override\n  String get keepCustomSettings => '保持自定义设置';\n\n  @override\n  String get editRequest => '编辑请求';\n\n  @override\n  String get reSendRequest => '已重新发送请求';\n\n  @override\n  String get viewExport => '视图导出';\n\n  @override\n  String get timeDesc => '按时间降序';\n\n  @override\n  String get timeAsc => '按时间升序';\n\n  @override\n  String get search => '搜索';\n\n  @override\n  String get clearSearch => '清除搜索';\n\n  @override\n  String get requestType => '请求类型';\n\n  @override\n  String get keyword => '关键词';\n\n  @override\n  String get keywordSearchScope => '关键词搜索范围: ';\n\n  @override\n  String get favorite => '收藏';\n\n  @override\n  String get deleteFavorite => '删除收藏';\n\n  @override\n  String get emptyFavorite => '暂无收藏';\n\n  @override\n  String get deleteFavoriteSuccess => '已删除收藏';\n\n  @override\n  String get historyRecord => '历史记录';\n\n  @override\n  String get historyCacheTime => '缓存时间';\n\n  @override\n  String get historyManualSave => '手动保存';\n\n  @override\n  String historyDay(Object day) {\n    return '$day天';\n  }\n\n  @override\n  String get historyForever => '永久';\n\n  @override\n  String historyRecordTitle(Object length, Object name) {\n    return '$name 记录数 $length';\n  }\n\n  @override\n  String get historyEmptyName => '名称不能为空';\n\n  @override\n  String historySubtitle(Object requestLength, Object size) {\n    return '记录数 $requestLength  文件 $size';\n  }\n\n  @override\n  String get historyUnSave => '当前会话记录未保存';\n\n  @override\n  String get historyDeleteConfirm => '是否删除该历史记录？';\n\n  @override\n  String get requestEdit => '请求编辑';\n\n  @override\n  String get encode => '编码';\n\n  @override\n  String get requestBody => '请求体';\n\n  @override\n  String get responseBody => '响应体';\n\n  @override\n  String get requestRewrite => '请求重写';\n\n  @override\n  String get newWindow => '新窗口打开';\n\n  @override\n  String get httpRequest => 'HTTP请求';\n\n  @override\n  String get enabledHttps => '启用HTTPS代理';\n\n  @override\n  String get installRootCa => '安装根证书';\n\n  @override\n  String get installCaLocal => '安装根证书到本机';\n\n  @override\n  String get downloadRootCa => '下载根证书';\n\n  @override\n  String get downloadRootCaNote => '注意：如果您将默认浏览器设置为 Safari 以外的浏览器，请单击此行复制并粘贴 Safari 浏览器的链接';\n\n  @override\n  String get generateCA => '重新生成根证书';\n\n  @override\n  String get generateCADescribe => '您确定要生成新的根证书吗? 如果确认，\\n则需要重新安装并信任新的证书';\n\n  @override\n  String get resetDefaultCA => '重置默认根证书';\n\n  @override\n  String get resetDefaultCADescribe => '确定要重置为默认根证书吗? ProxyPin默认\\n根证书对所有用户都是相同的.';\n\n  @override\n  String get exportCaP12 => '导出根证书 (.p12)';\n\n  @override\n  String get importCaP12 => '导入根证书 (.p12)';\n\n  @override\n  String get trustCa => '信任证书';\n\n  @override\n  String get profileDownload => '已下载描述文件';\n\n  @override\n  String get exportCA => '导出根证书';\n\n  @override\n  String get exportPrivateKey => '导出私钥';\n\n  @override\n  String get install => '安装';\n\n  @override\n  String get installCaDescribe => '安装证书 设置 > 已下载描述文件 > 安装';\n\n  @override\n  String get trustCaDescribe => '信任证书 设置 > 通用 > 关于本机 > 证书信任设置';\n\n  @override\n  String get androidRoot => '系统证书 (ROOT设备)';\n\n  @override\n  String get androidRootMagisk =>\n      'Magisk模块: \\n安卓ROOT设备可以使用Magisk ProxyPinCA系统证书模块, 安装完重启手机后 在系统证书查看是否有ProxyPinCA证书，如果有说明证书安装成功。';\n\n  @override\n  String androidRootRename(Object name) {\n    return '模块不生效可以根据网上教程安装系统根证书, 根证书命名成 $name';\n  }\n\n  @override\n  String get androidRootCADownload => '下载系统根证书(.0)';\n\n  @override\n  String get androidUserCA => '用户证书';\n\n  @override\n  String get androidUserCATips => '提示：Android7+ 很多软件不会信任用户证书';\n\n  @override\n  String get androidUserCAInstall => '打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书';\n\n  @override\n  String get androidUserXposed => '推荐使用Xposed模块抓包(无需ROOT), 点击查看wiki';\n\n  @override\n  String get configWifiProxy => '配置手机Wi-Fi代理';\n\n  @override\n  String get caInstallGuide => '证书安装指南';\n\n  @override\n  String get caAndroidBrowser => '在 Android 设备上打开浏览器访问：';\n\n  @override\n  String get caIosBrowser => '在 iOS 设备上打开 Safari访问：';\n\n  @override\n  String get localIP => '本地IP ';\n\n  @override\n  String get mobileScan => '配置Wi-Fi代理或使用手机版扫描二维码';\n\n  @override\n  String get decode => '解码';\n\n  @override\n  String get encodeInput => '输入要转换的内容';\n\n  @override\n  String get encodeResult => '转换结果';\n\n  @override\n  String get encodeFail => '编码失败';\n\n  @override\n  String get decodeFail => '解码失败';\n\n  @override\n  String get shareUrl => '分享请求链接';\n\n  @override\n  String get shareCurl => '分享 cURL 请求';\n\n  @override\n  String get shareRequestResponse => '分享请求和响应';\n\n  @override\n  String get captureDetail => '抓包详情';\n\n  @override\n  String get proxyPinSoftware => 'ProxyPin全平台开源抓包软件';\n\n  @override\n  String get prompt => '提示';\n\n  @override\n  String get curlSchemeRequest => '识别到curl格式，是否转换为HTTP请求？';\n\n  @override\n  String get appExitTips => '再按一次退出程序';\n\n  @override\n  String get remoteConnectDisconnect => '检查远程连接失败，已断开';\n\n  @override\n  String get connect => '连接';\n\n  @override\n  String get reconnect => '重新连接';\n\n  @override\n  String remoteConnected(Object os) {\n    return '已连接$os，流量将转发到$os';\n  }\n\n  @override\n  String get remoteConnectForward => '远程连接，将其他设备流量转发到当前设备';\n\n  @override\n  String get connectSuccess => '连接成功';\n\n  @override\n  String get connectedRemote => '已连接远程';\n\n  @override\n  String get connected => '已连接';\n\n  @override\n  String get notConnected => '未连接';\n\n  @override\n  String get disconnect => '断开连接';\n\n  @override\n  String get ipLayerProxy => 'IP层代理(Beta)';\n\n  @override\n  String get ipLayerProxyDesc => 'IP层代理可抓取Flutter应用请求，目前不是很稳定,欢迎提交PR';\n\n  @override\n  String get inputAddress => '输入地址';\n\n  @override\n  String get syncConfig => '同步配置';\n\n  @override\n  String get pullConfigFail => '拉取配置失败, 请检查网络连接';\n\n  @override\n  String get sync => '同步';\n\n  @override\n  String get invalidQRCode => '无法识别的二维码';\n\n  @override\n  String get remoteConnectFail => '连接失败，请检查是否在同一局域网和防火墙是否允许, ios需要开启本地网络权限';\n\n  @override\n  String get remoteConnectSuccessTips => '手机需要开启抓包才可以抓取请求哦';\n\n  @override\n  String get windowMode => '窗口模式';\n\n  @override\n  String get windowModeSubTitle => '开启抓包后 如果应用退回到后台，显示一个小窗口';\n\n  @override\n  String get pipIcon => '窗口快捷图标';\n\n  @override\n  String get pipIconDescribe => '展示快捷进入小窗口Icon';\n\n  @override\n  String get headerExpanded => 'Headers自动展开';\n\n  @override\n  String get headerExpandedSubtitle => '详情页Headers栏是否自动展开';\n\n  @override\n  String get bottomNavigation => '底部导航';\n\n  @override\n  String get bottomNavigationSubtitle => '底部导航栏是否显示，重启后生效';\n\n  @override\n  String get memoryCleanup => '内存清理';\n\n  @override\n  String get memoryCleanupSubtitle => '到内存限制自动清理请求，清理后保留最近32条请求';\n\n  @override\n  String get unlimited => '无限制';\n\n  @override\n  String get custom => '自定义';\n\n  @override\n  String get externalProxyAuth => '代理认证 (可选)';\n\n  @override\n  String get externalProxyServer => '代理服务器';\n\n  @override\n  String get externalProxyConnectFailure => '外部代理连接失败';\n\n  @override\n  String get externalProxyFailureConfirm => '网络不通所有接口将会访问失败，是否继续设置外部代理。';\n\n  @override\n  String get mobileDisplayPacketCapture => '手机端是否展示抓包:';\n\n  @override\n  String proxyPortRepeat(Object port) {\n    return '启动失败，请检查端口号$port是否被占用';\n  }\n\n  @override\n  String get reset => '重置';\n\n  @override\n  String get proxyIgnoreDomain => '代理忽略域名';\n\n  @override\n  String get domainWhitelistDescribe => '只代理白名单中的域名, 白名单启用黑名单将会失效';\n\n  @override\n  String get domainBlacklistDescribe => '黑名单中的域名不会代理';\n\n  @override\n  String get domain => '域名';\n\n  @override\n  String get enableScript => '启用脚本工具';\n\n  @override\n  String get scriptUseDescribe => '使用 JavaScript 修改请求和响应';\n\n  @override\n  String get scriptEdit => '编辑脚本';\n\n  @override\n  String get scrollEnd => '跟踪滚动';\n\n  @override\n  String get logger => '日志';\n\n  @override\n  String get material3 => 'Material3是谷歌开源设计系统的最新版本';\n\n  @override\n  String get iosVpnBackgroundAudio => '开启抓包后，退出到后台。为了维护主UI线程的网络通信，将启用静音音频播放以保持主线程运行。否则，它将只在后台运行30秒。您同意在启用抓包后在后台播放音频吗?';\n\n  @override\n  String get markRead => '标记已读';\n\n  @override\n  String get autoRead => '自动已读';\n\n  @override\n  String get highlight => '高亮';\n\n  @override\n  String get blue => '蓝色';\n\n  @override\n  String get green => '绿色';\n\n  @override\n  String get yellow => '黄色';\n\n  @override\n  String get red => '红色';\n\n  @override\n  String get pink => '粉色';\n\n  @override\n  String get gray => '灰色';\n\n  @override\n  String get underline => '下划线';\n\n  @override\n  String get requestBlock => '请求屏蔽';\n\n  @override\n  String get other => '其他';\n\n  @override\n  String get certHashName => '证书Hash名称';\n\n  @override\n  String get regExp => '正则表达式';\n\n  @override\n  String get systemCertName => '系统证书名称';\n\n  @override\n  String get qrCode => '二维码';\n\n  @override\n  String get scanQrCode => '扫描二维码';\n\n  @override\n  String get generateQrCode => '生成二维码';\n\n  @override\n  String get saveImage => '保存图片';\n\n  @override\n  String get selectImage => '选择图片';\n\n  @override\n  String get inputContent => '输入内容';\n\n  @override\n  String get errorCorrectLevel => '纠错等级';\n\n  @override\n  String get output => '输出';\n\n  @override\n  String get timestamp => '时间戳';\n\n  @override\n  String get convert => '转换';\n\n  @override\n  String get time => '时间';\n\n  @override\n  String get nowTimestamp => '当前时间戳(秒)';\n\n  @override\n  String get hosts => 'Hosts 映射';\n\n  @override\n  String get toAddress => '映射地址';\n\n  @override\n  String get encrypt => '加密';\n\n  @override\n  String get decrypt => '解密';\n\n  @override\n  String get cipher => '加解密';\n\n  @override\n  String get appUpdateCheckVersion => '检查更新';\n\n  @override\n  String get appUpdateNotAvailableMsg => '已是最新版本';\n\n  @override\n  String get appUpdateDialogTitle => '有可用更新';\n\n  @override\n  String get appUpdateUpdateMsg => 'ProxyPin 的新版本现已推出。您想现在更新吗？';\n\n  @override\n  String get appUpdateCurrentVersionLbl => '当前版本';\n\n  @override\n  String get appUpdateNewVersionLbl => '新版本';\n\n  @override\n  String get appUpdateUpdateNowBtnTxt => '现在更新';\n\n  @override\n  String get appUpdateLaterBtnTxt => '以后再说';\n\n  @override\n  String get appUpdateIgnoreBtnTxt => '忽略';\n\n  @override\n  String get requestMap => '请求映射';\n\n  @override\n  String get requestMapDescribe => '不请求远程服务，使用本地配置或脚本进行响应';\n\n  @override\n  String get automatic => '自动';\n\n  @override\n  String get manual => '手动';\n\n  @override\n  String get certNotInstalled => '证书未安装';\n\n  @override\n  String get openNewWindow => '新窗口打开';\n\n  @override\n  String get sponsorDonate => '赞助 / 捐赠';\n\n  @override\n  String get sponsorSupport => '支持项目持续开发';\n\n  @override\n  String get sponsorThanks => '感谢支持开源项目，可选择以下任意方式，帮助项目长期发展';\n\n  @override\n  String get sponsorAfdian => '爱发电赞助';\n\n  @override\n  String get sponsorBuyMeCoffee => 'Buy Me a Coffee';\n\n  @override\n  String get privacyPolicy => '隐私协议';\n\n  @override\n  String get privacyContent =>\n      '本项目为开源抓包工具，所有功能均在本地设备上运行；无任何后端服务器，不会收集、存储或上传任何用户信息。抓取的网络数据仅在本地处理，除非您主动使用远程转发功能。所需权限（如网络、存储、相机用于扫码）仅用于实现相应功能。您可在公开的源代码中审计其行为。';\n\n  @override\n  String get requestCrypto => '请求解密';\n\n  @override\n  String get cryptoDecoded => '已解密';\n\n  @override\n  String get cryptoDecodeToggle => '解密';\n\n  @override\n  String get optional => '可选';\n\n  @override\n  String get cryptoRuleField => '字段名称';\n\n  @override\n  String get cryptoIvPrefixLabel => 'IV 前缀';\n\n  @override\n  String get cryptoIvPrefixTooltip => '使用响应体前 N 个字节作为 IV';\n\n  @override\n  String get local => '本地';\n\n  @override\n  String get remoteUrl => '远程URL';\n\n  @override\n  String get view => '查看';\n}\n\n/// The translations for Chinese, using the Han script (`zh_Hant`).\nclass AppLocalizationsZhHant extends AppLocalizationsZh {\n  AppLocalizationsZhHant() : super('zh_Hant');\n\n  @override\n  String get breakpoint => '斷點';\n\n  @override\n  String get breakpointRule => '斷點規則';\n\n  @override\n  String get name => '名稱';\n\n  @override\n  String get requests => '抓包';\n\n  @override\n  String get favorites => '收藏';\n\n  @override\n  String get history => '歷史';\n\n  @override\n  String get toolbox => '工具箱';\n\n  @override\n  String get preference => '偏好設定';\n\n  @override\n  String get feedback => '意見回饋';\n\n  @override\n  String get about => '關於';\n\n  @override\n  String get filter => '代理過濾';\n\n  @override\n  String get script => '腳本';\n\n  @override\n  String get share => '分享';\n\n  @override\n  String get port => '連接埠號: ';\n\n  @override\n  String get proxy => '代理';\n\n  @override\n  String get externalProxy => '外部代理設定';\n\n  @override\n  String get username => '使用者名稱';\n\n  @override\n  String get password => '密碼';\n\n  @override\n  String get proxySetting => '代理設定';\n\n  @override\n  String get setAs => '設定為';\n\n  @override\n  String get systemProxy => '系統代理';\n\n  @override\n  String get enabledHTTP2 => '啟用HTTP2';\n\n  @override\n  String get serverNotStart => '未開啟抓包';\n\n  @override\n  String get download => '下載';\n\n  @override\n  String get config => '設定';\n\n  @override\n  String get version => '版本';\n\n  @override\n  String get start => '開始';\n\n  @override\n  String get stop => '停止';\n\n  @override\n  String get clear => '清空';\n\n  @override\n  String get httpsProxy => 'HTTPS 代理';\n\n  @override\n  String get setting => '設定';\n\n  @override\n  String get mobileConnect => '手機連接';\n\n  @override\n  String get connectRemote => '連接終端';\n\n  @override\n  String get remoteDevice => '遠端裝置';\n\n  @override\n  String get remoteDeviceList => '遠端裝置列表';\n\n  @override\n  String get myQRCode => '我的二維碼';\n\n  @override\n  String get theme => '主題';\n\n  @override\n  String get followSystem => '跟隨系統';\n\n  @override\n  String get themeColor => '主題顏色';\n\n  @override\n  String get themeLight => '淺色';\n\n  @override\n  String get themeDark => '深色';\n\n  @override\n  String get language => '語言';\n\n  @override\n  String get autoStartup => '自動開啟抓包';\n\n  @override\n  String get autoStartupDescribe => '程式啟動時自動開始記錄流量';\n\n  @override\n  String get copied => '已複製到剪切板';\n\n  @override\n  String get execute => '執行';\n\n  @override\n  String get cancel => '取消';\n\n  @override\n  String get close => '關閉';\n\n  @override\n  String get save => '儲存';\n\n  @override\n  String get confirm => '確認';\n\n  @override\n  String get confirmTitle => '確認操作';\n\n  @override\n  String get confirmContent => '是否確認此操作?';\n\n  @override\n  String get addSuccess => '新增成功';\n\n  @override\n  String get saveSuccess => '儲存成功';\n\n  @override\n  String get operationSuccess => '操作成功';\n\n  @override\n  String get import => '匯入';\n\n  @override\n  String get importSuccess => '匯入成功';\n\n  @override\n  String get importFailed => '匯入失敗';\n\n  @override\n  String get export => '匯出';\n\n  @override\n  String get exportSuccess => '匯出成功';\n\n  @override\n  String get deleteSuccess => '刪除成功';\n\n  @override\n  String get send => '傳送';\n\n  @override\n  String get fail => '失敗';\n\n  @override\n  String get success => '成功';\n\n  @override\n  String get emptyData => '無資料';\n\n  @override\n  String get requestSuccess => '請求成功';\n\n  @override\n  String get add => '新增';\n\n  @override\n  String get all => '全部';\n\n  @override\n  String get modify => '修改';\n\n  @override\n  String get responseType => '回應類型';\n\n  @override\n  String get request => '請求';\n\n  @override\n  String get response => '回應';\n\n  @override\n  String get statusCode => '狀態碼';\n\n  @override\n  String get duration => '耗時';\n\n  @override\n  String get done => '完成';\n\n  @override\n  String get type => '類型';\n\n  @override\n  String get enable => '啟用';\n\n  @override\n  String get example => '範例: ';\n\n  @override\n  String get responseHeader => '回應標頭';\n\n  @override\n  String get requestHeader => '請求標頭';\n\n  @override\n  String get requestLine => '請求行';\n\n  @override\n  String get requestMethod => '請求方法';\n\n  @override\n  String get param => '參數';\n\n  @override\n  String get replaceBodyWith => '訊息體替換為:';\n\n  @override\n  String get redirectTo => '重新導向到:';\n\n  @override\n  String get redirect => '重新導向';\n\n  @override\n  String get cannotBeEmpty => '不能為空';\n\n  @override\n  String get requestRewriteList => '請求重寫列表';\n\n  @override\n  String get requestRewriteRule => '請求重寫規則';\n\n  @override\n  String get requestRewriteEnable => '是否啟用請求重寫';\n\n  @override\n  String get action => '行為';\n\n  @override\n  String get multiple => '多選';\n\n  @override\n  String get edit => '編輯';\n\n  @override\n  String get disabled => '停用';\n\n  @override\n  String requestRewriteDeleteConfirm(Object size) {\n    return '是否刪除$size條規則?';\n  }\n\n  @override\n  String get useGuide => '使用文件';\n\n  @override\n  String get pleaseEnter => '請輸入';\n\n  @override\n  String get click => '點選';\n\n  @override\n  String get replace => '替換';\n\n  @override\n  String get clickEdit => '點選編輯';\n\n  @override\n  String get refresh => '重新整理';\n\n  @override\n  String get selectFile => '選擇檔案';\n\n  @override\n  String get match => '符合';\n\n  @override\n  String get value => '值';\n\n  @override\n  String get matchRule => '符合規則';\n\n  @override\n  String get emptyMatchAll => '為空表示符合全部';\n\n  @override\n  String get newBuilt => '新建';\n\n  @override\n  String get reportServers => '上報伺服器';\n\n  @override\n  String get addReportServer => '新增上報伺服器';\n\n  @override\n  String get editReportServer => '編輯上報伺服器';\n\n  @override\n  String get serverUrl => '伺服器 URL';\n\n  @override\n  String get compression => '壓縮';\n\n  @override\n  String get compressionNone => '無';\n\n  @override\n  String get newFolder => '新建資料夾';\n\n  @override\n  String get enableSelect => '啟用選擇';\n\n  @override\n  String get disableSelect => '停用選擇';\n\n  @override\n  String get deleteSelect => '刪除選擇';\n\n  @override\n  String get testData => '測試資料';\n\n  @override\n  String get noChangesDetected => '未檢測到變更';\n\n  @override\n  String get enterMatchData => '輸入待符合的資料';\n\n  @override\n  String get modifyRequestHeader => '修改請求標頭';\n\n  @override\n  String get headerName => '請求標頭名稱';\n\n  @override\n  String get headerValue => '請求標頭值';\n\n  @override\n  String get deleteHeaderConfirm => '是否刪除該請求標頭';\n\n  @override\n  String get sequence => '全部請求';\n\n  @override\n  String get domainList => '網域名稱列表';\n\n  @override\n  String get domainWhitelist => '代理網域名稱白名單';\n\n  @override\n  String get domainBlacklist => '代理網域名稱黑名單';\n\n  @override\n  String get domainFilter => '網域名稱代理列表';\n\n  @override\n  String get appWhitelist => '應用程式白名單';\n\n  @override\n  String get appWhitelistDescribe => '只代理白名單中的應用程式, 白名單啟用黑名單將會失效';\n\n  @override\n  String get appBlacklist => '應用程式黑名單';\n\n  @override\n  String get scanCode => '掃碼連接';\n\n  @override\n  String get addBlacklist => '新增代理黑名單';\n\n  @override\n  String get addWhitelist => '新增代理白名單';\n\n  @override\n  String get deleteWhitelist => '刪除代理白名單';\n\n  @override\n  String domainListSubtitle(Object count, Object time) {\n    return '最後請求時間: $time,  次數: $count';\n  }\n\n  @override\n  String get selectAction => '選擇操作';\n\n  @override\n  String get copy => '複製';\n\n  @override\n  String get copyHost => '複製網域名稱';\n\n  @override\n  String get copyUrl => '複製URL';\n\n  @override\n  String get copyRawRequest => '複製原始請求';\n\n  @override\n  String get copyRequestResponse => '複製 請求和回應';\n\n  @override\n  String get copyCurl => '複製 cURL';\n\n  @override\n  String get copyAsPythonRequests => '複製 Python Requests';\n\n  @override\n  String get delete => '刪除';\n\n  @override\n  String get rename => '重新命名';\n\n  @override\n  String get repeat => '重放';\n\n  @override\n  String get repeatAllRequests => '重放所有請求';\n\n  @override\n  String get repeatDomainRequests => '重放網域名稱下請求';\n\n  @override\n  String get customRepeat => '進階重放';\n\n  @override\n  String get repeatCount => '次數';\n\n  @override\n  String get repeatInterval => '間隔(ms)';\n\n  @override\n  String get repeatDelay => '延遲(ms)';\n\n  @override\n  String get scheduleTime => '指定時間';\n\n  @override\n  String get fixed => '固定';\n\n  @override\n  String get random => '隨機';\n\n  @override\n  String get keepCustomSettings => '保持自訂設定';\n\n  @override\n  String get editRequest => '編輯請求';\n\n  @override\n  String get reSendRequest => '已重新傳送請求';\n\n  @override\n  String get viewExport => '檢視匯出';\n\n  @override\n  String get timeDesc => '按時間降序';\n\n  @override\n  String get timeAsc => '按時間升序';\n\n  @override\n  String get search => '搜尋';\n\n  @override\n  String get clearSearch => '清除搜尋';\n\n  @override\n  String get requestType => '請求類型';\n\n  @override\n  String get keyword => '關鍵字';\n\n  @override\n  String get keywordSearchScope => '關鍵字搜尋範圍: ';\n\n  @override\n  String get favorite => '收藏';\n\n  @override\n  String get deleteFavorite => '刪除收藏';\n\n  @override\n  String get emptyFavorite => '暫無收藏';\n\n  @override\n  String get deleteFavoriteSuccess => '已刪除收藏';\n\n  @override\n  String get historyRecord => '歷史記錄';\n\n  @override\n  String get historyCacheTime => '快取時間';\n\n  @override\n  String get historyManualSave => '手動儲存';\n\n  @override\n  String historyDay(Object day) {\n    return '$day天';\n  }\n\n  @override\n  String get historyForever => '永久';\n\n  @override\n  String historyRecordTitle(Object length, Object name) {\n    return '$name 記錄數 $length';\n  }\n\n  @override\n  String get historyEmptyName => '名稱不能為空';\n\n  @override\n  String historySubtitle(Object requestLength, Object size) {\n    return '記錄數 $requestLength  檔案 $size';\n  }\n\n  @override\n  String get historyUnSave => '目前對話記錄未儲存';\n\n  @override\n  String get historyDeleteConfirm => '是否刪除該歷史記錄？';\n\n  @override\n  String get requestEdit => '請求編輯';\n\n  @override\n  String get encode => '編碼';\n\n  @override\n  String get requestBody => '請求體';\n\n  @override\n  String get responseBody => '回應體';\n\n  @override\n  String get requestRewrite => '請求重寫';\n\n  @override\n  String get newWindow => '新視窗開啟';\n\n  @override\n  String get httpRequest => 'HTTP請求';\n\n  @override\n  String get enabledHttps => '啟用HTTPS代理';\n\n  @override\n  String get installRootCa => '安裝根憑證';\n\n  @override\n  String get installCaLocal => '安裝根憑證到本機';\n\n  @override\n  String get downloadRootCa => '下載根憑證';\n\n  @override\n  String get downloadRootCaNote => '注意：如果您將預設瀏覽器設定為 Safari 以外的瀏覽器，請點選此行複製並貼上 Safari 瀏覽器的連結';\n\n  @override\n  String get generateCA => '重新產生根憑證';\n\n  @override\n  String get generateCADescribe => '您確定要產生新的根憑證嗎? 如果確認，\\n則需要重新安裝並信任新的憑證';\n\n  @override\n  String get resetDefaultCA => '重置預設根憑證';\n\n  @override\n  String get resetDefaultCADescribe => '確定要重置為預設根憑證嗎? ProxyPin預設\\n根憑證對所有使用者都是相同的.';\n\n  @override\n  String get exportCaP12 => '匯出根憑證 (.p12)';\n\n  @override\n  String get importCaP12 => '匯入根憑證 (.p12)';\n\n  @override\n  String get trustCa => '信任憑證';\n\n  @override\n  String get profileDownload => '已下載描述檔案';\n\n  @override\n  String get exportCA => '匯出根憑證';\n\n  @override\n  String get exportPrivateKey => '匯出私鑰';\n\n  @override\n  String get install => '安裝';\n\n  @override\n  String get installCaDescribe => '安裝憑證 設定 > 已下載描述檔案 > 安裝';\n\n  @override\n  String get trustCaDescribe => '信任憑證 設定 > 一般 > 關於本機 > 憑證信任設定';\n\n  @override\n  String get androidRoot => '系統憑證 (ROOT裝置)';\n\n  @override\n  String get androidRootMagisk =>\n      'Magisk模組: \\n安卓ROOT裝置可以使用Magisk ProxyPinCA系統憑證模組, 安裝完重新開機後 在系統憑證檢視是否有ProxyPinCA憑證，如果有說明憑證安裝成功。';\n\n  @override\n  String androidRootRename(Object name) {\n    return '模組不生效可以根據網上教學安裝系統根憑證, 根憑證命名成 $name';\n  }\n\n  @override\n  String get androidRootCADownload => '下載系統根憑證(.0)';\n\n  @override\n  String get androidUserCA => '使用者憑證';\n\n  @override\n  String get androidUserCATips => '提示：Android7+ 很多軟體不會信任使用者憑證';\n\n  @override\n  String get androidUserCAInstall => '開啟設定 -> 安全性 -> 加密和憑證 -> 安裝憑證 -> CA 憑證';\n\n  @override\n  String get androidUserXposed => '推薦使用Xposed模組抓包(無需ROOT), 點選檢視wiki';\n\n  @override\n  String get configWifiProxy => '設定手機Wi-Fi代理';\n\n  @override\n  String get caInstallGuide => '憑證安裝指南';\n\n  @override\n  String get caAndroidBrowser => '在 Android 裝置上開啟瀏覽器存取：';\n\n  @override\n  String get caIosBrowser => '在 iOS 裝置上開啟 Safari存取：';\n\n  @override\n  String get localIP => '本機IP ';\n\n  @override\n  String get mobileScan => '設定Wi-Fi代理或使用手機版掃描二維碼';\n\n  @override\n  String get decode => '解碼';\n\n  @override\n  String get encodeInput => '輸入要轉換的內容';\n\n  @override\n  String get encodeResult => '轉換結果';\n\n  @override\n  String get encodeFail => '編碼失敗';\n\n  @override\n  String get decodeFail => '解碼失敗';\n\n  @override\n  String get shareUrl => '分享請求連結';\n\n  @override\n  String get shareCurl => '分享 cURL 請求';\n\n  @override\n  String get shareRequestResponse => '分享請求和回應';\n\n  @override\n  String get captureDetail => '抓包詳情';\n\n  @override\n  String get proxyPinSoftware => 'ProxyPin全平台開源抓包軟體';\n\n  @override\n  String get prompt => '提示';\n\n  @override\n  String get curlSchemeRequest => '識別到curl格式，是否轉換為HTTP請求？';\n\n  @override\n  String get appExitTips => '再按一次退出程式';\n\n  @override\n  String get remoteConnectDisconnect => '檢查遠端連接失敗，已中斷連接';\n\n  @override\n  String get connect => '連接';\n\n  @override\n  String get reconnect => '重新連接';\n\n  @override\n  String remoteConnected(Object os) {\n    return '已連接$os，流量將轉發到$os';\n  }\n\n  @override\n  String get remoteConnectForward => '遠端連接，將其他裝置流量轉發到目前裝置';\n\n  @override\n  String get connectSuccess => '連接成功';\n\n  @override\n  String get connectedRemote => '已連接遠端';\n\n  @override\n  String get connected => '已連接';\n\n  @override\n  String get notConnected => '未連接';\n\n  @override\n  String get disconnect => '中斷連接';\n\n  @override\n  String get ipLayerProxy => 'IP層代理(Beta)';\n\n  @override\n  String get ipLayerProxyDesc => 'IP層代理可抓取Flutter應用程式請求，目前不是很穩定,歡迎提交PR';\n\n  @override\n  String get inputAddress => '輸入地址';\n\n  @override\n  String get syncConfig => '同步設定';\n\n  @override\n  String get pullConfigFail => '拉取設定失敗, 請檢查網路連接';\n\n  @override\n  String get sync => '同步';\n\n  @override\n  String get invalidQRCode => '無法識別的二維碼';\n\n  @override\n  String get remoteConnectFail => '連接失敗，請檢查是否在同一區域網路和防火牆是否允許, ios需要開啟本機網路權限';\n\n  @override\n  String get remoteConnectSuccessTips => '手機需要開啟抓包才可以抓取請求哦';\n\n  @override\n  String get windowMode => '視窗模式';\n\n  @override\n  String get windowModeSubTitle => '開啟抓包後 如果應用程式退回到背景，顯示一個小視窗';\n\n  @override\n  String get pipIcon => '視窗快捷圖示';\n\n  @override\n  String get pipIconDescribe => '展示快捷進入小視窗Icon';\n\n  @override\n  String get headerExpanded => 'Headers自動展開';\n\n  @override\n  String get headerExpandedSubtitle => '詳情頁Headers欄是否自動展開';\n\n  @override\n  String get bottomNavigation => '底部導航';\n\n  @override\n  String get bottomNavigationSubtitle => '底部導航欄是否顯示，重新啟動後生效';\n\n  @override\n  String get memoryCleanup => '記憶體清理';\n\n  @override\n  String get memoryCleanupSubtitle => '到記憶體限制自動清理請求，清理後保留最近32條請求';\n\n  @override\n  String get unlimited => '無限制';\n\n  @override\n  String get custom => '自訂';\n\n  @override\n  String get externalProxyAuth => '代理認證 (可選)';\n\n  @override\n  String get externalProxyServer => '代理伺服器';\n\n  @override\n  String get externalProxyConnectFailure => '外部代理連接失敗';\n\n  @override\n  String get externalProxyFailureConfirm => '網路不通所有介面將會存取失敗，是否繼續設定外部代理。';\n\n  @override\n  String get mobileDisplayPacketCapture => '手機端是否展示抓包:';\n\n  @override\n  String proxyPortRepeat(Object port) {\n    return '啟動失敗，請檢查連接埠號$port是否被占用';\n  }\n\n  @override\n  String get reset => '重置';\n\n  @override\n  String get proxyIgnoreDomain => '代理忽略網域名稱';\n\n  @override\n  String get domainWhitelistDescribe => '只代理白名單中的網域名稱, 白名單啟用黑名單將會失效';\n\n  @override\n  String get domainBlacklistDescribe => '黑名單中的網域名稱不會代理';\n\n  @override\n  String get domain => '網域名稱';\n\n  @override\n  String get enableScript => '啟用腳本工具';\n\n  @override\n  String get scriptUseDescribe => '使用 JavaScript 修改請求和回應';\n\n  @override\n  String get scriptEdit => '編輯腳本';\n\n  @override\n  String get scrollEnd => '跟蹤滾動';\n\n  @override\n  String get logger => '日誌';\n\n  @override\n  String get material3 => 'Material3是Google開源設計系統的最新版本';\n\n  @override\n  String get iosVpnBackgroundAudio =>\n      '開啟抓包後，退出到背景。為了維護主UI執行緒的網路通信，將啟用靜音音訊播放以保持主執行緒運作。否則，它將只在背景運作30秒。您同意在啟用抓包後在背景播放音訊嗎?';\n\n  @override\n  String get markRead => '標記已讀';\n\n  @override\n  String get autoRead => '自動已讀';\n\n  @override\n  String get highlight => '高亮顯示';\n\n  @override\n  String get blue => '藍色';\n\n  @override\n  String get green => '綠色';\n\n  @override\n  String get yellow => '黃色';\n\n  @override\n  String get red => '紅色';\n\n  @override\n  String get pink => '粉色';\n\n  @override\n  String get gray => '灰色';\n\n  @override\n  String get underline => '底線';\n\n  @override\n  String get requestBlock => '請求阻擋';\n\n  @override\n  String get other => '其他';\n\n  @override\n  String get certHashName => '憑證Hash名稱';\n\n  @override\n  String get regExp => '正規表示式';\n\n  @override\n  String get systemCertName => '系統憑證名稱';\n\n  @override\n  String get qrCode => '二維碼';\n\n  @override\n  String get scanQrCode => '掃描二維碼';\n\n  @override\n  String get generateQrCode => '產生二維碼';\n\n  @override\n  String get saveImage => '儲存圖片';\n\n  @override\n  String get selectImage => '選擇圖片';\n\n  @override\n  String get inputContent => '輸入內容';\n\n  @override\n  String get errorCorrectLevel => '糾錯等級';\n\n  @override\n  String get output => '輸出';\n\n  @override\n  String get timestamp => '時間戳';\n\n  @override\n  String get convert => '轉換';\n\n  @override\n  String get time => '時間';\n\n  @override\n  String get nowTimestamp => '目前時間戳(秒)';\n\n  @override\n  String get hosts => 'Hosts 對應';\n\n  @override\n  String get toAddress => '對應地址';\n\n  @override\n  String get encrypt => '加密';\n\n  @override\n  String get decrypt => '解密';\n\n  @override\n  String get cipher => '密文';\n\n  @override\n  String get appUpdateCheckVersion => '檢查更新';\n\n  @override\n  String get appUpdateNotAvailableMsg => '已是最新版本';\n\n  @override\n  String get appUpdateDialogTitle => '有可用更新';\n\n  @override\n  String get appUpdateUpdateMsg => 'ProxyPin 的新版本現已推出。您想現在更新嗎？';\n\n  @override\n  String get appUpdateCurrentVersionLbl => '目前版本';\n\n  @override\n  String get appUpdateNewVersionLbl => '新版本';\n\n  @override\n  String get appUpdateUpdateNowBtnTxt => '現在更新';\n\n  @override\n  String get appUpdateLaterBtnTxt => '稍後再說';\n\n  @override\n  String get appUpdateIgnoreBtnTxt => '忽略';\n\n  @override\n  String get requestMap => '請求映射';\n\n  @override\n  String get requestMapDescribe => '不請求遠端服務，使用本地配置或腳本進行回應';\n\n  @override\n  String get automatic => '自動';\n\n  @override\n  String get manual => '手動';\n\n  @override\n  String get certNotInstalled => '未安裝憑證';\n\n  @override\n  String get openNewWindow => '新視窗開啟';\n\n  @override\n  String get sponsorDonate => '贊助 / 捐贈';\n\n  @override\n  String get sponsorSupport => '支持項目持續開發';\n\n  @override\n  String get sponsorThanks => '感謝支持開源項目，可選擇以下任意方式，幫助項目長期發展';\n\n  @override\n  String get sponsorAfdian => '愛發電贊助';\n\n  @override\n  String get sponsorBuyMeCoffee => 'Buy Me a Coffee';\n\n  @override\n  String get privacyPolicy => '隱私協議';\n\n  @override\n  String get privacyContent =>\n      '本專案為開源抓包工具，所有功能均在本機裝置上運行；無任何後端伺服器，不會蒐集、儲存或上傳任何使用者資訊。擷取的網路資料僅在本機處理，除非您主動使用遠端轉發功能。所需權限（如網路、儲存、相機用於掃碼）僅用於實現相應功能。您可在公開的原始碼中稽核其行為。';\n\n  @override\n  String get requestCrypto => '請求解密';\n\n  @override\n  String get cryptoDecoded => '已解密';\n\n  @override\n  String get cryptoDecodeToggle => '解密';\n\n  @override\n  String get optional => '可選';\n\n  @override\n  String get cryptoRuleField => '字段';\n\n  @override\n  String get cryptoIvPrefixLabel => 'IV 前綴';\n\n  @override\n  String get cryptoIvPrefixTooltip => '使用回應內容的前 N 個字節作為 IV';\n\n  @override\n  String get local => '本地';\n\n  @override\n  String get remoteUrl => '遠端URL';\n\n  @override\n  String get view => '檢視';\n}\n"
  },
  {
    "path": "lib/l10n/app_zh.arb",
    "content": "{\n\n  \"breakpoint\": \"断点\",\n  \"breakpointRule\": \"断点规则\",\n  \"name\": \"名称\",\n  \"requests\": \"抓包\",\n  \"favorites\": \"收藏\",\n  \"history\": \"历史\",\n  \"toolbox\": \"工具箱\",\n  \"preference\": \"偏好设置\",\n  \"feedback\": \"反馈\",\n  \"about\": \"关于\",\n  \"filter\": \"代理过滤\",\n  \"script\": \"脚本\",\n  \"share\": \"分享\",\n  \"port\": \"端口号: \",\n  \"proxy\": \"代理\",\n  \"externalProxy\": \"外部代理设置\",\n  \"username\": \"用户名\",\n  \"password\": \"密码\",\n  \"proxySetting\": \"代理设置\",\n  \"setAs\": \"设置为\",\n  \"systemProxy\": \"系统代理\",\n  \"enabledHTTP2\": \"启用HTTP2\",\n  \"serverNotStart\": \"未开启抓包\",\n  \"download\": \"下载\",\n  \"config\": \"配置\",\n  \"version\": \"版本\",\n\n  \"start\": \"开始\",\n  \"stop\": \"停止\",\n  \"clear\": \"清空\",\n  \"httpsProxy\": \"HTTPS 代理\",\n  \"setting\": \"设置\",\n  \"mobileConnect\": \"手机连接\",\n  \"connectRemote\": \"连接终端\",\n  \"remoteDevice\": \"远程设备\",\n  \"remoteDeviceList\": \"远程设备列表\",\n  \"myQRCode\": \"我的二维码\",\n\n  \"theme\": \"主题\",\n  \"themeColor\": \"主题颜色\",\n  \"followSystem\": \"跟随系统\",\n  \"themeLight\": \"浅色\",\n  \"themeDark\": \"深色\",\n  \"language\": \"语言\",\n  \"autoStartup\": \"自动开启抓包\",\n  \"autoStartupDescribe\": \"程序启动时自动开始记录流量\",\n\n  \"copied\": \"已复制到剪切板\",\n  \"execute\": \"执行\",\n  \"cancel\": \"取消\",\n  \"close\": \"关闭\",\n  \"save\": \"保存\",\n  \"confirm\": \"确认\",\n  \"confirmTitle\": \"确认操作\",\n  \"confirmContent\": \"是否确认此操作?\",\n  \"addSuccess\": \"添加成功\",\n  \"saveSuccess\": \"保存成功\",\n  \"operationSuccess\": \"操作成功\",\n  \"import\": \"导入\",\n  \"importSuccess\": \"导入成功\",\n  \"importFailed\": \"导入失败\",\n  \"export\": \"导出\",\n  \"exportSuccess\": \"导出成功\",\n  \"exportFailed\": \"导出失败\",\n  \"deleteSuccess\": \"删除成功\",\n  \"send\": \"发送\",\n  \"fail\": \"失败\",\n  \"success\": \"成功\",\n  \"emptyData\": \"无数据\",\n  \"requestSuccess\": \"请求成功\",\n  \"add\": \"添加\",\n  \"all\": \"全部\",\n  \"modify\": \"修改\",\n  \"responseType\": \"响应类型\",\n  \"request\": \"请求\",\n  \"response\": \"响应\",\n  \"statusCode\": \"状态码\",\n  \"duration\": \"耗时\",\n\n  \"done\": \"完成\",\n  \"type\": \"类型\",\n  \"enable\": \"启用\",\n  \"example\": \"示例: \",\n  \"responseHeader\": \"响应头\",\n  \"requestHeader\": \"请求头\",\n  \"requestLine\": \"请求行\",\n  \"requestMethod\": \"请求方法\",\n  \"param\": \"参数\",\n  \"replaceBodyWith\": \"消息体替换为:\",\n  \"redirectTo\": \"重定向到:\",\n  \"redirect\": \"重定向\",\n  \"cannotBeEmpty\": \"不能为空\",\n  \"requestRewriteList\": \"请求重写列表\",\n  \"requestRewriteRule\": \"请求重写规则\",\n  \"requestRewriteEnable\": \"是否启用请求重写\",\n  \"action\": \"行为\",\n  \"multiple\": \"多选\",\n  \"edit\": \"编辑\",\n  \"disabled\": \"禁用\",\n  \"requestRewriteDeleteConfirm\": \"是否删除{size}条规则?\",\n  \"useGuide\": \"使用文档\",\n  \"pleaseEnter\": \"请输入\",\n  \"click\": \"点击\",\n  \"replace\": \"替换\",\n  \"clickEdit\": \"点击编辑\",\n  \"refresh\": \"刷新\",\n  \"selectFile\": \"选择文件\",\n  \"match\": \"匹配\",\n  \"value\": \"值\",\n  \"matchRule\": \"匹配规则\",\n  \"emptyMatchAll\": \"为空表示匹配全部\",\n  \"newBuilt\": \"新建\",\n  \"reportServers\": \"上报服务器\",\n  \"addReportServer\": \"新增上报服务器\",\n  \"editReportServer\": \"编辑上报服务器\",\n  \"serverUrl\": \"服务器 URL\",\n  \"compression\": \"压缩\",\n  \"compressionNone\": \"无\",\n  \"newFolder\": \"新建文件夹\",\n  \"enableSelect\": \"启用选择\",\n  \"disableSelect\": \"禁用选择\",\n  \"deleteSelect\": \"删除选择\",\n  \"testData\": \"测试数据\",\n  \"noChangesDetected\": \"未检测到变更\",\n  \"enterMatchData\": \"输入待匹配的数据\",\n\n  \"modifyRequestHeader\": \"修改请求头\",\n  \"headerName\": \"请求头名称\",\n  \"headerValue\": \"请求头值\",\n  \"deleteHeaderConfirm\": \"是否删除该请求头\",\n\n  \"sequence\": \"全部请求\",\n  \"domainList\": \"域名列表\",\n  \"domainWhitelist\": \"代理域名白名单\",\n  \"domainBlacklist\": \"代理域名黑名单\",\n  \"appWhitelist\": \"应用白名单\",\n  \"appWhitelistDescribe\": \"只代理白名单中的应用, 白名单启用黑名单将会失效\",\n  \"appBlacklist\": \"应用黑名单\",\n  \"domainFilter\": \"域名代理列表\",\n  \"scanCode\": \"扫码连接\",\n  \"addBlacklist\": \"添加代理黑名单\",\n  \"addWhitelist\": \"添加代理白名单\",\n  \"deleteWhitelist\": \"删除代理白名单\",\n  \"domainListSubtitle\": \"最后请求时间: {time},  次数: {count}\",\n\n  \"selectAction\": \"选择操作\",\n  \"copy\": \"复制\",\n  \"copyHost\": \"复制域名\",\n  \"copyUrl\": \"复制URL\",\n  \"copyRawRequest\": \"复制 原始请求\",\n  \"copyRequestResponse\": \"复制 请求和响应\",\n  \"copyCurl\": \"复制 cURL\",\n  \"copyAsPythonRequests\": \"复制 Python Requests\",\n  \"delete\": \"删除\",\n  \"rename\": \"重命名\",\n  \"repeat\": \"重放\",\n  \"repeatAllRequests\": \"重放所有请求\",\n  \"repeatDomainRequests\": \"重放域名下请求\",\n  \"customRepeat\": \"高级重放\",\n  \"repeatCount\": \"次数\",\n  \"repeatInterval\": \"间隔(ms)\",\n  \"repeatDelay\": \"延时(ms)\",\n  \"scheduleTime\": \"指定时间\",\n  \"fixed\": \"固定\",\n  \"random\": \"随机\",\n  \"keepCustomSettings\": \"保持自定义设置\",\n  \"editRequest\": \"编辑请求\",\n  \"reSendRequest\": \"已重新发送请求\",\n  \"viewExport\": \"视图导出\",\n  \"timeDesc\": \"按时间降序\",\n  \"timeAsc\": \"按时间升序\",\n\n  \"search\": \"搜索\",\n  \"clearSearch\": \"清除搜索\",\n  \"requestType\": \"请求类型\",\n  \"keyword\": \"关键词\",\n  \"keywordSearchScope\": \"关键词搜索范围: \",\n\n  \"favorite\": \"收藏\",\n  \"deleteFavorite\": \"删除收藏\",\n  \"emptyFavorite\": \"暂无收藏\",\n  \"deleteFavoriteSuccess\": \"已删除收藏\",\n\n  \"historyRecord\": \"历史记录\",\n  \"historyManualSave\": \"手动保存\",\n  \"historyDay\": \"{day}天\",\n  \"historyForever\": \"永久\",\n  \"historyCacheTime\": \"缓存时间\",\n  \"historyEmptyName\": \"名称不能为空\",\n  \"historyRecordTitle\": \"{name} 记录数 {length}\",\n  \"historySubtitle\": \"记录数 {requestLength}  文件 {size}\",\n  \"historyUnSave\": \"当前会话记录未保存\",\n  \"historyDeleteConfirm\": \"是否删除该历史记录？\",\n\n  \"requestEdit\": \"请求编辑\",\n  \"encode\": \"编码\",\n  \"decode\": \"解码\",\n  \"requestBody\": \"请求体\",\n  \"responseBody\": \"响应体\",\n  \"requestRewrite\": \"请求重写\",\n  \"newWindow\": \"新窗口打开\",\n  \"httpRequest\": \"HTTP请求\",\n\n  \"enabledHttps\": \"启用HTTPS代理\",\n  \"installRootCa\": \"安装根证书\",\n  \"installCaLocal\": \"安装根证书到本机\",\n  \"downloadRootCa\": \"下载根证书\",\n  \"downloadRootCaNote\": \"注意：如果您将默认浏览器设置为 Safari 以外的浏览器，请单击此行复制并粘贴 Safari 浏览器的链接\",\n  \"generateCA\": \"重新生成根证书\",\n  \"generateCADescribe\": \"您确定要生成新的根证书吗? 如果确认，\\n则需要重新安装并信任新的证书\",\n  \"resetDefaultCA\": \"重置默认根证书\",\n  \"resetDefaultCADescribe\": \"确定要重置为默认根证书吗? ProxyPin默认\\n根证书对所有用户都是相同的.\",\n  \"exportCaP12\": \"导出根证书 (.p12)\",\n  \"importCaP12\": \"导入根证书 (.p12)\",\n  \"trustCa\": \"信任证书\",\n  \"exportCA\": \"导出根证书\",\n  \"exportPrivateKey\": \"导出私钥\",\n  \"profileDownload\": \"已下载描述文件\",\n  \"install\": \"安装\",\n  \"installCaDescribe\": \"安装证书 设置 > 已下载描述文件 > 安装\",\n  \"trustCaDescribe\": \"信任证书 设置 > 通用 > 关于本机 > 证书信任设置\",\n  \"androidRoot\": \"系统证书 (ROOT设备)\",\n  \"androidRootMagisk\": \"Magisk模块: \\n安卓ROOT设备可以使用Magisk ProxyPinCA系统证书模块, 安装完重启手机后 在系统证书查看是否有ProxyPinCA证书，如果有说明证书安装成功。\",\n  \"androidRootRename\": \"模块不生效可以根据网上教程安装系统根证书, 根证书命名成 {name}\",\n  \"androidUserCA\": \"用户证书\",\n  \"androidUserCATips\": \"提示：Android7+ 很多软件不会信任用户证书\",\n  \"androidRootCADownload\": \"下载系统根证书(.0)\",\n  \"androidUserCAInstall\": \"打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书\",\n  \"androidUserXposed\": \"推荐使用Xposed模块抓包(无需ROOT), 点击查看wiki\",\n  \"configWifiProxy\": \"配置手机Wi-Fi代理\",\n  \"caInstallGuide\": \"证书安装指南\",\n  \"caAndroidBrowser\": \"在 Android 设备上打开浏览器访问：\",\n  \"caIosBrowser\": \"在 iOS 设备上打开 Safari访问：\",\n\n  \"localIP\": \"本地IP \",\n  \"mobileScan\": \"配置Wi-Fi代理或使用手机版扫描二维码\",\n\n  \"encodeInput\": \"输入要转换的内容\",\n  \"encodeResult\": \"转换结果\",\n  \"encodeFail\": \"编码失败\",\n  \"decodeFail\": \"解码失败\",\n\n  \"shareUrl\": \"分享请求链接\",\n  \"shareCurl\": \"分享 cURL 请求\",\n  \"shareRequestResponse\": \"分享请求和响应\",\n  \"captureDetail\": \"抓包详情\",\n  \"proxyPinSoftware\": \"ProxyPin全平台开源抓包软件\",\n\n  \"prompt\": \"提示\",\n  \"curlSchemeRequest\": \"识别到curl格式，是否转换为HTTP请求？\",\n  \"appExitTips\": \"再按一次退出程序\",\n  \"remoteConnectDisconnect\": \"检查远程连接失败，已断开\",\n  \"connect\": \"连接\",\n  \"reconnect\": \"重新连接\",\n  \"remoteConnected\": \"已连接{os}，流量将转发到{os}\",\n  \"remoteConnectForward\": \"远程连接，将其他设备流量转发到当前设备\",\n  \"connectSuccess\": \"连接成功\",\n  \"connectedRemote\": \"已连接远程\",\n  \"connected\": \"已连接\",\n  \"notConnected\": \"未连接\",\n  \"inputAddress\": \"输入地址\",\n  \"disconnect\": \"断开连接\",\n  \"ipLayerProxy\": \"IP层代理(Beta)\",\n  \"ipLayerProxyDesc\": \"IP层代理可抓取Flutter应用请求，目前不是很稳定,欢迎提交PR\",\n  \"syncConfig\": \"同步配置\",\n  \"pullConfigFail\": \"拉取配置失败, 请检查网络连接\",\n  \"sync\": \"同步\",\n  \"invalidQRCode\": \"无法识别的二维码\",\n  \"remoteConnectFail\": \"连接失败，请检查是否在同一局域网和防火墙是否允许, ios需要开启本地网络权限\",\n  \"remoteConnectSuccessTips\": \"手机需要开启抓包才可以抓取请求哦\",\n\n  \"windowMode\": \"窗口模式\",\n  \"windowModeSubTitle\": \"开启抓包后 如果应用退回到后台，显示一个小窗口\",\n  \"pipIcon\": \"窗口快捷图标\",\n  \"pipIconDescribe\": \"展示快捷进入小窗口Icon\",\n  \"headerExpanded\": \"Headers自动展开\",\n  \"headerExpandedSubtitle\": \"详情页Headers栏是否自动展开\",\n  \"bottomNavigation\": \"底部导航\",\n  \"bottomNavigationSubtitle\": \"底部导航栏是否显示，重启后生效\",\n  \"memoryCleanup\": \"内存清理\",\n  \"memoryCleanupSubtitle\": \"到内存限制自动清理请求，清理后保留最近32条请求\",\n  \"unlimited\": \"无限制\",\n  \"custom\": \"自定义\",\n\n  \"externalProxyAuth\": \"代理认证 (可选)\",\n  \"externalProxyServer\": \"代理服务器\",\n  \"externalProxyConnectFailure\": \"外部代理连接失败\",\n  \"externalProxyFailureConfirm\": \"网络不通所有接口将会访问失败，是否继续设置外部代理。\",\n  \"mobileDisplayPacketCapture\": \"手机端是否展示抓包:\",\n  \"proxyPortRepeat\": \"启动失败，请检查端口号{port}是否被占用\",\n  \"reset\": \"重置\",\n  \"proxyIgnoreDomain\": \"代理忽略域名\",\n  \"domainWhitelistDescribe\": \"只代理白名单中的域名, 白名单启用黑名单将会失效\",\n  \"domainBlacklistDescribe\": \"黑名单中的域名不会代理\",\n  \"domain\": \"域名\",\n  \"enableScript\": \"启用脚本工具\",\n  \"scriptUseDescribe\": \"使用 JavaScript 修改请求和响应\",\n  \"scriptEdit\": \"编辑脚本\",\n  \"scrollEnd\": \"跟踪滚动\",\n  \"logger\": \"日志\",\n  \"material3\": \"Material3是谷歌开源设计系统的最新版本\",\n  \"iosVpnBackgroundAudio\": \"开启抓包后，退出到后台。为了维护主UI线程的网络通信，将启用静音音频播放以保持主线程运行。否则，它将只在后台运行30秒。您同意在启用抓包后在后台播放音频吗?\",\n\n  \"markRead\": \"标记已读\",\n  \"autoRead\": \"自动已读\",\n  \"highlight\": \"高亮\",\n  \"blue\": \"蓝色\",\n  \"green\": \"绿色\",\n  \"yellow\": \"黄色\",\n  \"red\": \"红色\",\n  \"pink\": \"粉色\",\n  \"gray\": \"灰色\",\n  \"underline\": \"下划线\",\n\n  \"requestBlock\": \"请求屏蔽\",\n\n  \"other\": \"其他\",\n  \"certHashName\": \"证书Hash名称\",\n  \"systemCertName\": \"系统证书名称\",\n  \"regExp\": \"正则表达式\",\n  \"qrCode\": \"二维码\",\n  \"generateQrCode\": \"生成二维码\",\n  \"scanQrCode\": \"扫描二维码\",\n  \"saveImage\": \"保存图片\",\n  \"selectImage\": \"选择图片\",\n  \"inputContent\": \"输入内容\",\n  \"errorCorrectLevel\": \"纠错等级\",\n  \"output\": \"输出\",\n  \"timestamp\": \"时间戳\",\n  \"convert\": \"转换\",\n  \"time\": \"时间\",\n  \"nowTimestamp\": \"当前时间戳(秒)\",\n  \"hosts\": \"Hosts 映射\",\n  \"toAddress\": \"映射地址\",\n  \"encrypt\": \"加密\",\n  \"decrypt\": \"解密\",\n  \"cipher\": \"加解密\",\n\n  \"appUpdateCheckVersion\": \"检查更新\",\n  \"appUpdateNotAvailableMsg\": \"已是最新版本\",\n  \"appUpdateDialogTitle\": \"有可用更新\",\n  \"appUpdateUpdateMsg\": \"ProxyPin 的新版本现已推出。您想现在更新吗？\",\n  \"appUpdateCurrentVersionLbl\": \"当前版本\",\n  \"appUpdateNewVersionLbl\": \"新版本\",\n  \"appUpdateUpdateNowBtnTxt\": \"现在更新\",\n  \"appUpdateLaterBtnTxt\": \"以后再说\",\n  \"appUpdateIgnoreBtnTxt\": \"忽略\",\n\n  \"requestMap\": \"请求映射\",\n  \"requestMapDescribe\": \"不请求远程服务，使用本地配置或脚本进行响应\",\n\n  \"automatic\": \"自动\",\n  \"manual\": \"手动\",\n  \"certNotInstalled\": \"证书未安装\",\n\n  \"openNewWindow\": \"新窗口打开\",\n  \"sponsorDonate\": \"赞助 / 捐赠\",\n  \"sponsorSupport\": \"支持项目持续开发\",\n  \"sponsorThanks\": \"感谢支持开源项目，可选择以下任意方式，帮助项目长期发展\",\n  \"sponsorAfdian\": \"爱发电赞助\",\n  \"sponsorBuyMeCoffee\": \"Buy Me a Coffee\",\n\n  \"privacyPolicy\": \"隐私协议\",\n  \"privacyContent\": \"本项目为开源抓包工具，所有功能均在本地设备上运行；无任何后端服务器，不会收集、存储或上传任何用户信息。抓取的网络数据仅在本地处理，除非您主动使用远程转发功能。所需权限（如网络、存储、相机用于扫码）仅用于实现相应功能。您可在公开的源代码中审计其行为。\",\n\n  \"requestCrypto\": \"请求解密\",\n  \"cryptoDecoded\": \"已解密\",\n  \"cryptoDecodeToggle\": \"解密\",\n  \"optional\": \"可选\",\n  \"cryptoRuleField\": \"字段名称\",\n\n  \"cryptoIvPrefixLabel\": \"IV 前缀\",\n  \"cryptoIvPrefixTooltip\": \"使用响应体前 N 个字节作为 IV\",\n\n  \"local\": \"本地\",\n  \"remoteUrl\": \"远程URL\",\n  \"view\": \"查看\"\n}"
  },
  {
    "path": "lib/l10n/app_zh_Hant.arb",
    "content": "{\n  \"breakpoint\": \"斷點\",\n  \"breakpointRule\": \"斷點規則\",\n  \"requests\": \"抓包\",\n  \"favorites\": \"收藏\",\n  \"history\": \"歷史\",\n  \"toolbox\": \"工具箱\",\n  \"preference\": \"偏好設定\",\n  \"feedback\": \"意見回饋\",\n  \"about\": \"關於\",\n  \"filter\": \"代理過濾\",\n  \"script\": \"腳本\",\n  \"share\": \"分享\",\n  \"port\": \"連接埠號: \",\n  \"proxy\": \"代理\",\n  \"externalProxy\": \"外部代理設定\",\n  \"username\": \"使用者名稱\",\n  \"password\": \"密碼\",\n  \"proxySetting\": \"代理設定\",\n  \"setAs\": \"設定為\",\n  \"systemProxy\": \"系統代理\",\n  \"enabledHTTP2\": \"啟用HTTP2\",\n  \"serverNotStart\": \"未開啟抓包\",\n  \"download\": \"下載\",\n  \"config\": \"設定\",\n  \"version\": \"版本\",\n\n  \"start\": \"開始\",\n  \"stop\": \"停止\",\n  \"clear\": \"清空\",\n  \"httpsProxy\": \"HTTPS 代理\",\n  \"setting\": \"設定\",\n  \"mobileConnect\": \"手機連接\",\n  \"connectRemote\": \"連接終端\",\n  \"remoteDevice\": \"遠端裝置\",\n  \"remoteDeviceList\": \"遠端裝置列表\",\n  \"myQRCode\": \"我的二維碼\",\n  \"theme\": \"主題\",\n  \"themeColor\": \"主題顏色\",\n  \"followSystem\": \"跟隨系統\",\n  \"themeLight\": \"淺色\",\n  \"themeDark\": \"深色\",\n  \"language\": \"語言\",\n  \"autoStartup\": \"自動開啟抓包\",\n  \"autoStartupDescribe\": \"程式啟動時自動開始記錄流量\",\n  \"copied\": \"已複製到剪切板\",\n  \"execute\": \"執行\",\n  \"cancel\": \"取消\",\n  \"close\": \"關閉\",\n  \"save\": \"儲存\",\n  \"confirm\": \"確認\",\n  \"confirmTitle\": \"確認操作\",\n  \"confirmContent\": \"是否確認此操作?\",\n  \"addSuccess\": \"新增成功\",\n  \"saveSuccess\": \"儲存成功\",\n  \"operationSuccess\": \"操作成功\",\n  \"import\": \"匯入\",\n  \"importSuccess\": \"匯入成功\",\n  \"importFailed\": \"匯入失敗\",\n  \"export\": \"匯出\",\n  \"exportSuccess\": \"匯出成功\",\n  \"deleteSuccess\": \"刪除成功\",\n  \"send\": \"傳送\",\n  \"fail\": \"失敗\",\n  \"success\": \"成功\",\n  \"emptyData\": \"無資料\",\n  \"requestSuccess\": \"請求成功\",\n  \"add\": \"新增\",\n  \"all\": \"全部\",\n  \"modify\": \"修改\",\n  \"responseType\": \"回應類型\",\n  \"request\": \"請求\",\n  \"response\": \"回應\",\n  \"statusCode\": \"狀態碼\",\n  \"duration\": \"耗時\",\n\n  \"done\": \"完成\",\n  \"type\": \"類型\",\n  \"enable\": \"啟用\",\n  \"example\": \"範例: \",\n  \"responseHeader\": \"回應標頭\",\n  \"requestHeader\": \"請求標頭\",\n  \"requestLine\": \"請求行\",\n  \"requestMethod\": \"請求方法\",\n  \"param\": \"參數\",\n  \"replaceBodyWith\": \"訊息體替換為:\",\n  \"redirectTo\": \"重新導向到:\",\n  \"redirect\": \"重新導向\",\n  \"cannotBeEmpty\": \"不能為空\",\n  \"requestRewriteList\": \"請求重寫列表\",\n  \"requestRewriteRule\": \"請求重寫規則\",\n  \"requestRewriteEnable\": \"是否啟用請求重寫\",\n  \"action\": \"行為\",\n  \"multiple\": \"多選\",\n  \"edit\": \"編輯\",\n  \"disabled\": \"停用\",\n  \"requestRewriteDeleteConfirm\": \"是否刪除{size}條規則?\",\n  \"useGuide\": \"使用文件\",\n  \"pleaseEnter\": \"請輸入\",\n  \"click\": \"點選\",\n  \"replace\": \"替換\",\n  \"clickEdit\": \"點選編輯\",\n  \"refresh\": \"重新整理\",\n  \"selectFile\": \"選擇檔案\",\n  \"match\": \"符合\",\n  \"value\": \"值\",\n  \"matchRule\": \"符合規則\",\n  \"emptyMatchAll\": \"為空表示符合全部\",\n  \"newBuilt\": \"新建\",\n  \"reportServers\": \"上報伺服器\",\n  \"addReportServer\": \"新增上報伺服器\",\n  \"editReportServer\": \"編輯上報伺服器\",\n  \"serverUrl\": \"伺服器 URL\",\n  \"compression\": \"壓縮\",\n  \"compressionNone\": \"無\",\n  \"newFolder\": \"新建資料夾\",\n  \"enableSelect\": \"啟用選擇\",\n  \"disableSelect\": \"停用選擇\",\n  \"deleteSelect\": \"刪除選擇\",\n  \"testData\": \"測試資料\",\n  \"noChangesDetected\": \"未檢測到變更\",\n  \"enterMatchData\": \"輸入待符合的資料\",\n  \"modifyRequestHeader\": \"修改請求標頭\",\n  \"headerName\": \"請求標頭名稱\",\n  \"headerValue\": \"請求標頭值\",\n  \"deleteHeaderConfirm\": \"是否刪除該請求標頭\",\n  \"sequence\": \"全部請求\",\n  \"domainList\": \"網域名稱列表\",\n  \"domainWhitelist\": \"代理網域名稱白名單\",\n  \"domainBlacklist\": \"代理網域名稱黑名單\",\n  \"appWhitelist\": \"應用程式白名單\",\n  \"appWhitelistDescribe\": \"只代理白名單中的應用程式, 白名單啟用黑名單將會失效\",\n  \"appBlacklist\": \"應用程式黑名單\",\n  \"domainFilter\": \"網域名稱代理列表\",\n  \"scanCode\": \"掃碼連接\",\n  \"addBlacklist\": \"新增代理黑名單\",\n  \"addWhitelist\": \"新增代理白名單\",\n  \"deleteWhitelist\": \"刪除代理白名單\",\n  \"domainListSubtitle\": \"最後請求時間: {time},  次數: {count}\",\n  \"selectAction\": \"選擇操作\",\n  \"copy\": \"複製\",\n  \"copyHost\": \"複製網域名稱\",\n  \"copyUrl\": \"複製URL\",\n  \"copyRawRequest\": \"複製原始請求\",\n  \"copyRequestResponse\": \"複製 請求和回應\",\n  \"copyCurl\": \"複製 cURL\",\n  \"copyAsPythonRequests\": \"複製 Python Requests\",\n  \"delete\": \"刪除\",\n  \"rename\": \"重新命名\",\n  \"repeat\": \"重放\",\n  \"repeatAllRequests\": \"重放所有請求\",\n  \"repeatDomainRequests\": \"重放網域名稱下請求\",\n  \"customRepeat\": \"進階重放\",\n  \"repeatCount\": \"次數\",\n  \"repeatInterval\": \"間隔(ms)\",\n  \"repeatDelay\": \"延遲(ms)\",\n  \"scheduleTime\": \"指定時間\",\n  \"fixed\": \"固定\",\n  \"random\": \"隨機\",\n  \"keepCustomSettings\": \"保持自訂設定\",\n  \"editRequest\": \"編輯請求\",\n  \"reSendRequest\": \"已重新傳送請求\",\n  \"viewExport\": \"檢視匯出\",\n  \"timeDesc\": \"按時間降序\",\n  \"timeAsc\": \"按時間升序\",\n  \"search\": \"搜尋\",\n  \"clearSearch\": \"清除搜尋\",\n  \"requestType\": \"請求類型\",\n  \"keyword\": \"關鍵字\",\n  \"keywordSearchScope\": \"關鍵字搜尋範圍: \",\n  \"favorite\": \"收藏\",\n  \"deleteFavorite\": \"刪除收藏\",\n  \"emptyFavorite\": \"暫無收藏\",\n  \"deleteFavoriteSuccess\": \"已刪除收藏\",\n  \"name\": \"名稱\",\n  \"historyRecord\": \"歷史記錄\",\n  \"historyManualSave\": \"手動儲存\",\n  \"historyDay\": \"{day}天\",\n  \"historyForever\": \"永久\",\n  \"historyCacheTime\": \"快取時間\",\n  \"historyEmptyName\": \"名稱不能為空\",\n  \"historyRecordTitle\": \"{name} 記錄數 {length}\",\n  \"historySubtitle\": \"記錄數 {requestLength}  檔案 {size}\",\n  \"historyUnSave\": \"目前對話記錄未儲存\",\n  \"historyDeleteConfirm\": \"是否刪除該歷史記錄？\",\n  \"requestEdit\": \"請求編輯\",\n  \"encode\": \"編碼\",\n  \"decode\": \"解碼\",\n  \"requestBody\": \"請求體\",\n  \"responseBody\": \"回應體\",\n  \"requestRewrite\": \"請求重寫\",\n  \"newWindow\": \"新視窗開啟\",\n  \"httpRequest\": \"HTTP請求\",\n  \"enabledHttps\": \"啟用HTTPS代理\",\n  \"installRootCa\": \"安裝根憑證\",\n  \"installCaLocal\": \"安裝根憑證到本機\",\n  \"downloadRootCa\": \"下載根憑證\",\n  \"downloadRootCaNote\": \"注意：如果您將預設瀏覽器設定為 Safari 以外的瀏覽器，請點選此行複製並貼上 Safari 瀏覽器的連結\",\n  \"generateCA\": \"重新產生根憑證\",\n  \"generateCADescribe\": \"您確定要產生新的根憑證嗎? 如果確認，\\n則需要重新安裝並信任新的憑證\",\n  \"resetDefaultCA\": \"重置預設根憑證\",\n  \"resetDefaultCADescribe\": \"確定要重置為預設根憑證嗎? ProxyPin預設\\n根憑證對所有使用者都是相同的.\",\n  \"exportCaP12\": \"匯出根憑證 (.p12)\",\n  \"importCaP12\": \"匯入根憑證 (.p12)\",\n  \"trustCa\": \"信任憑證\",\n  \"exportCA\": \"匯出根憑證\",\n  \"exportPrivateKey\": \"匯出私鑰\",\n  \"profileDownload\": \"已下載描述檔案\",\n  \"install\": \"安裝\",\n  \"installCaDescribe\": \"安裝憑證 設定 > 已下載描述檔案 > 安裝\",\n  \"trustCaDescribe\": \"信任憑證 設定 > 一般 > 關於本機 > 憑證信任設定\",\n  \"androidRoot\": \"系統憑證 (ROOT裝置)\",\n  \"androidRootMagisk\": \"Magisk模組: \\n安卓ROOT裝置可以使用Magisk ProxyPinCA系統憑證模組, 安裝完重新開機後 在系統憑證檢視是否有ProxyPinCA憑證，如果有說明憑證安裝成功。\",\n  \"androidRootRename\": \"模組不生效可以根據網上教學安裝系統根憑證, 根憑證命名成 {name}\",\n  \"androidUserCA\": \"使用者憑證\",\n  \"androidUserCATips\": \"提示：Android7+ 很多軟體不會信任使用者憑證\",\n  \"androidRootCADownload\": \"下載系統根憑證(.0)\",\n  \"androidUserCAInstall\": \"開啟設定 -> 安全性 -> 加密和憑證 -> 安裝憑證 -> CA 憑證\",\n  \"androidUserXposed\": \"推薦使用Xposed模組抓包(無需ROOT), 點選檢視wiki\",\n  \"configWifiProxy\": \"設定手機Wi-Fi代理\",\n  \"caInstallGuide\": \"憑證安裝指南\",\n  \"caAndroidBrowser\": \"在 Android 裝置上開啟瀏覽器存取：\",\n  \"caIosBrowser\": \"在 iOS 裝置上開啟 Safari存取：\",\n  \"localIP\": \"本機IP \",\n  \"mobileScan\": \"設定Wi-Fi代理或使用手機版掃描二維碼\",\n  \"encodeInput\": \"輸入要轉換的內容\",\n  \"encodeResult\": \"轉換結果\",\n  \"encodeFail\": \"編碼失敗\",\n  \"decodeFail\": \"解碼失敗\",\n  \"shareUrl\": \"分享請求連結\",\n  \"shareCurl\": \"分享 cURL 請求\",\n  \"shareRequestResponse\": \"分享請求和回應\",\n  \"captureDetail\": \"抓包詳情\",\n  \"proxyPinSoftware\": \"ProxyPin全平台開源抓包軟體\",\n  \"prompt\": \"提示\",\n  \"curlSchemeRequest\": \"識別到curl格式，是否轉換為HTTP請求？\",\n  \"appExitTips\": \"再按一次退出程式\",\n  \"remoteConnectDisconnect\": \"檢查遠端連接失敗，已中斷連接\",\n  \"connect\": \"連接\",\n  \"reconnect\": \"重新連接\",\n  \"remoteConnected\": \"已連接{os}，流量將轉發到{os}\",\n  \"remoteConnectForward\": \"遠端連接，將其他裝置流量轉發到目前裝置\",\n  \"connectSuccess\": \"連接成功\",\n  \"connectedRemote\": \"已連接遠端\",\n  \"connected\": \"已連接\",\n  \"notConnected\": \"未連接\",\n  \"inputAddress\": \"輸入地址\",\n  \"disconnect\": \"中斷連接\",\n  \"ipLayerProxy\": \"IP層代理(Beta)\",\n  \"ipLayerProxyDesc\": \"IP層代理可抓取Flutter應用程式請求，目前不是很穩定,歡迎提交PR\",\n  \"syncConfig\": \"同步設定\",\n  \"pullConfigFail\": \"拉取設定失敗, 請檢查網路連接\",\n  \"sync\": \"同步\",\n  \"invalidQRCode\": \"無法識別的二維碼\",\n  \"remoteConnectFail\": \"連接失敗，請檢查是否在同一區域網路和防火牆是否允許, ios需要開啟本機網路權限\",\n  \"remoteConnectSuccessTips\": \"手機需要開啟抓包才可以抓取請求哦\",\n  \"windowMode\": \"視窗模式\",\n  \"windowModeSubTitle\": \"開啟抓包後 如果應用程式退回到背景，顯示一個小視窗\",\n  \"pipIcon\": \"視窗快捷圖示\",\n  \"pipIconDescribe\": \"展示快捷進入小視窗Icon\",\n  \"headerExpanded\": \"Headers自動展開\",\n  \"headerExpandedSubtitle\": \"詳情頁Headers欄是否自動展開\",\n  \"bottomNavigation\": \"底部導航\",\n  \"bottomNavigationSubtitle\": \"底部導航欄是否顯示，重新啟動後生效\",\n  \"memoryCleanup\": \"記憶體清理\",\n  \"memoryCleanupSubtitle\": \"到記憶體限制自動清理請求，清理後保留最近32條請求\",\n  \"unlimited\": \"無限制\",\n  \"custom\": \"自訂\",\n  \"externalProxyAuth\": \"代理認證 (可選)\",\n  \"externalProxyServer\": \"代理伺服器\",\n  \"externalProxyConnectFailure\": \"外部代理連接失敗\",\n  \"externalProxyFailureConfirm\": \"網路不通所有介面將會存取失敗，是否繼續設定外部代理。\",\n  \"mobileDisplayPacketCapture\": \"手機端是否展示抓包:\",\n  \"proxyPortRepeat\": \"啟動失敗，請檢查連接埠號{port}是否被占用\",\n  \"reset\": \"重置\",\n  \"proxyIgnoreDomain\": \"代理忽略網域名稱\",\n  \"domainWhitelistDescribe\": \"只代理白名單中的網域名稱, 白名單啟用黑名單將會失效\",\n  \"domainBlacklistDescribe\": \"黑名單中的網域名稱不會代理\",\n  \"domain\": \"網域名稱\",\n  \"enableScript\": \"啟用腳本工具\",\n  \"scriptUseDescribe\": \"使用 JavaScript 修改請求和回應\",\n  \"scriptEdit\": \"編輯腳本\",\n  \"scrollEnd\": \"跟蹤滾動\",\n  \"logger\": \"日誌\",\n  \"material3\": \"Material3是Google開源設計系統的最新版本\",\n  \"iosVpnBackgroundAudio\": \"開啟抓包後，退出到背景。為了維護主UI執行緒的網路通信，將啟用靜音音訊播放以保持主執行緒運作。否則，它將只在背景運作30秒。您同意在啟用抓包後在背景播放音訊嗎?\",\n  \"markRead\": \"標記已讀\",\n  \"autoRead\": \"自動已讀\",\n  \"highlight\": \"高亮顯示\",\n  \"blue\": \"藍色\",\n  \"green\": \"綠色\",\n  \"yellow\": \"黃色\",\n  \"red\": \"紅色\",\n  \"pink\": \"粉色\",\n  \"gray\": \"灰色\",\n  \"underline\": \"底線\",\n  \"requestBlock\": \"請求阻擋\",\n  \"other\": \"其他\",\n  \"certHashName\": \"憑證Hash名稱\",\n  \"systemCertName\": \"系統憑證名稱\",\n  \"regExp\": \"正規表示式\",\n  \"qrCode\": \"二維碼\",\n  \"generateQrCode\": \"產生二維碼\",\n  \"scanQrCode\": \"掃描二維碼\",\n  \"saveImage\": \"儲存圖片\",\n  \"selectImage\": \"選擇圖片\",\n  \"inputContent\": \"輸入內容\",\n  \"errorCorrectLevel\": \"糾錯等級\",\n  \"output\": \"輸出\",\n  \"timestamp\": \"時間戳\",\n  \"convert\": \"轉換\",\n  \"time\": \"時間\",\n  \"nowTimestamp\": \"目前時間戳(秒)\",\n  \"hosts\": \"Hosts 對應\",\n  \"toAddress\": \"對應地址\",\n  \"encrypt\": \"加密\",\n  \"decrypt\": \"解密\",\n  \"cipher\": \"密文\",\n  \"requestCrypto\": \"請求解密\",\n  \"cryptoDecoded\": \"已解密\",\n  \"cryptoDecodeToggle\": \"解密\",\n  \"optional\": \"可選\",\n  \"cryptoRuleField\": \"字段\",\n  \"cryptoIvPrefixLabel\": \"IV 前綴\",\n  \"cryptoIvPrefixTooltip\": \"使用回應內容的前 N 個字節作為 IV\",\n  \"appUpdateCheckVersion\": \"檢查更新\",\n  \"appUpdateNotAvailableMsg\": \"已是最新版本\",\n  \"appUpdateDialogTitle\": \"有可用更新\",\n  \"appUpdateUpdateMsg\": \"ProxyPin 的新版本現已推出。您想現在更新嗎？\",\n  \"appUpdateCurrentVersionLbl\": \"目前版本\",\n  \"appUpdateNewVersionLbl\": \"新版本\",\n  \"appUpdateUpdateNowBtnTxt\": \"現在更新\",\n  \"appUpdateLaterBtnTxt\": \"稍後再說\",\n  \"appUpdateIgnoreBtnTxt\": \"忽略\",\n\n  \"requestMap\": \"請求映射\",\n  \"requestMapDescribe\": \"不請求遠端服務，使用本地配置或腳本進行回應\",\n\n  \"automatic\": \"自動\",\n  \"manual\": \"手動\",\n  \"certNotInstalled\": \"未安裝憑證\",\n  \"openNewWindow\": \"新視窗開啟\",\n  \"sponsorDonate\": \"贊助 / 捐贈\",\n  \"sponsorSupport\": \"支持項目持續開發\",\n  \"sponsorThanks\": \"感謝支持開源項目，可選擇以下任意方式，幫助項目長期發展\",\n  \"sponsorAfdian\": \"愛發電贊助\",\n  \"sponsorBuyMeCoffee\": \"Buy Me a Coffee\",\n\n  \"privacyPolicy\": \"隱私協議\",\n  \"privacyContent\": \"本專案為開源抓包工具，所有功能均在本機裝置上運行；無任何後端伺服器，不會蒐集、儲存或上傳任何使用者資訊。擷取的網路資料僅在本機處理，除非您主動使用遠端轉發功能。所需權限（如網路、儲存、相機用於掃碼）僅用於實現相應功能。您可在公開的原始碼中稽核其行為。\",\n\n  \"local\": \"本地\",\n  \"remoteUrl\": \"遠端URL\",\n  \"view\": \"檢視\"\n}"
  },
  {
    "path": "lib/main.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/ui/component/chinese_font.dart';\nimport 'package:proxypin/ui/component/multi_window.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/desktop/desktop.dart';\nimport 'package:proxypin/ui/mobile/mobile.dart';\nimport 'package:proxypin/utils/desktop_support.dart';\nimport 'package:proxypin/utils/navigator.dart';\nimport 'package:proxypin/utils/platform.dart';\n\nimport 'l10n/app_localizations.dart';\n\n///主入口\n///@author wanghongen\nvoid main(List<String> args) async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  //多窗口\n  if (args.firstOrNull == 'multi_window') {\n    final windowId = int.parse(args[1]);\n    final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map<String, dynamic>;\n    runApp(FluentApp(multiWindow(windowId, argument), (await AppConfiguration.instance)));\n    return;\n  }\n\n  var instance = AppConfiguration.instance;\n  var configuration = Configuration.instance;\n  //移动端\n  if (Platforms.isMobile()) {\n    var appConfiguration = await instance;\n    runApp(FluentApp(MobileHomePage((await configuration), appConfiguration), appConfiguration));\n    return;\n  }\n\n  var appConfiguration = await instance;\n  if (Platforms.isDesktop()) {\n    await DesktopSupport.initialize(appConfiguration);\n  }\n\n  runApp(FluentApp(DesktopHomePage(await configuration, appConfiguration), appConfiguration));\n}\n\nclass FluentApp extends StatelessWidget {\n  final Widget home;\n  final AppConfiguration appConfiguration;\n\n  const FluentApp(this.home, this.appConfiguration, {super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder<bool>(\n        valueListenable: appConfiguration.globalChange,\n        builder: (_, current, __) {\n          return MaterialApp(\n            title: 'ProxyPin',\n            debugShowCheckedModeBanner: false,\n            navigatorKey: navigatorHelper.navigatorKey,\n            theme: theme(Brightness.light),\n            darkTheme: theme(Brightness.dark),\n            themeMode: appConfiguration.themeMode,\n            locale: appConfiguration.language,\n            localizationsDelegates: AppLocalizations.localizationsDelegates,\n            supportedLocales: AppLocalizations.supportedLocales,\n            home: home,\n          );\n        });\n  }\n\n  ThemeData theme(Brightness brightness) {\n    bool useMaterial3 = appConfiguration.useMaterial3;\n    bool isDark = brightness == Brightness.dark;\n\n    Color? themeColor = isDark ? appConfiguration.themeColor : appConfiguration.themeColor;\n    Color? cardColor = isDark ? Color(0XFF3C3C3C) : Colors.white;\n    Color? surfaceContainer = isDark ? Colors.grey[800] : Colors.white;\n\n    Color? secondary = useMaterial3 ? null : themeColor;\n    if (themeColor is MaterialColor) {\n      secondary = themeColor[500];\n    }\n\n    var colorScheme = ColorScheme.fromSeed(\n      brightness: brightness,\n      seedColor: themeColor,\n      primary: themeColor,\n      surface: cardColor,\n      secondary: secondary,\n      onPrimary: isDark ? Colors.white : null,\n      surfaceContainer: surfaceContainer,\n      surfaceContainerHigh: surfaceContainer,\n    );\n\n    var themeData =\n        ThemeData(brightness: brightness, useMaterial3: appConfiguration.useMaterial3, colorScheme: colorScheme);\n\n    if (!appConfiguration.useMaterial3) {\n      themeData = themeData.copyWith(\n        appBarTheme: themeData.appBarTheme.copyWith(\n          iconTheme: themeData.iconTheme.copyWith(size: 20),\n          backgroundColor: themeData.canvasColor,\n          elevation: 0,\n          titleTextStyle: themeData.textTheme.titleMedium,\n        ),\n        tabBarTheme: themeData.tabBarTheme.copyWith(\n          labelColor: themeData.colorScheme.primary,\n          indicatorColor: themeColor,\n          unselectedLabelColor: themeData.textTheme.titleMedium?.color,\n        ),\n      );\n    }\n\n    if (Platform.isWindows) {\n      themeData = themeData.useSystemChineseFont();\n    }\n\n    return themeData.copyWith(\n        dialogTheme:\n            themeData.dialogTheme.copyWith(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))));\n  }\n}\n"
  },
  {
    "path": "lib/native/app_lifecycle.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nabstract interface class LifecycleListener {\n  void onUserLeaveHint() {}\n\n  void onPictureInPictureModeChanged(bool isInPictureInPictureMode) {}\n}\n\nclass AppLifecycleBinding {\n  static const MethodChannel _methodChannel = MethodChannel('com.proxy/appLifecycle');\n\n  //单例对象\n  static AppLifecycleBinding get instance {\n    _instance ??= AppLifecycleBinding._();\n    return _instance!;\n  }\n\n  final List<LifecycleListener> _listeners = <LifecycleListener>[];\n\n  static AppLifecycleBinding? _instance;\n\n  AppLifecycleBinding._() {\n    //注册方法\n    _methodChannel.setMethodCallHandler(_methodCallHandler);\n  }\n\n  static AppLifecycleBinding ensureInitialized() {\n    return AppLifecycleBinding.instance;\n  }\n\n  void addListener(LifecycleListener listener) {\n    if (_listeners.contains(listener)) return;\n    _listeners.add(listener);\n  }\n\n  void removeListener(LifecycleListener listener) {\n    _listeners.remove(listener);\n  }\n\n  Future<void> _methodCallHandler(MethodCall call) async {\n    logger.d(\"AppLifecycle methodCallHandler ${call.method}\");\n    switch (call.method) {\n      case 'appDetached':\n        await WidgetsBinding.instance.handleRequestAppExit();\n        break;\n      case 'onUserLeaveHint':\n        for (var listener in _listeners) {\n          listener.onUserLeaveHint();\n        }\n        break;\n      case 'onPictureInPictureModeChanged':\n        for (var listener in _listeners) {\n          listener.onPictureInPictureModeChanged(call.arguments);\n        }\n        break;\n    }\n    return Future.value();\n  }\n}\n"
  },
  {
    "path": "lib/native/installed_apps.dart",
    "content": "import 'package:flutter/services.dart';\n\nclass InstalledApps {\n  static const MethodChannel _methodChannel = MethodChannel('com.proxy/installedApps');\n\n  static Future<List<AppInfo>> getInstalledApps(\n    bool withIcon, {\n    String? packageNamePrefix,\n    bool includeSystemApps = false,\n  }) {\n    return _methodChannel.invokeListMethod<Map>('getInstalledApps', {\n      \"withIcon\": withIcon,\n      \"packageNamePrefix\": packageNamePrefix,\n      \"includeSystemApps\": includeSystemApps,\n    }).then((value) => value?.map((e) => AppInfo.formJson(e)).toList() ?? []);\n  }\n\n  static Future<AppInfo> getAppInfo(String packageName) async {\n    return _methodChannel\n        .invokeMethod<Map>('getAppInfo', {\"packageName\": packageName}).then((value) => AppInfo.formJson(value!));\n  }\n}\n\nclass AppInfo {\n  String? name;\n  String? packageName;\n  String? versionName;\n\n  //icon\n  Uint8List? icon;\n\n  bool? inValid;\n\n  AppInfo({\n    this.name,\n    this.packageName,\n    this.versionName,\n    this.icon,\n    this.inValid,\n  });\n\n  AppInfo.formJson(Map<dynamic, dynamic> json) {\n    name = json['name'];\n    packageName = json['packageName'];\n    versionName = json['versionName'];\n    icon = json['icon'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n    data['name'] = name;\n    data['packageName'] = packageName;\n    data['versionName'] = versionName;\n    data['icon'] = icon;\n    return data;\n  }\n\n  @override\n  String toString() {\n    return toJson().toString();\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) {\n      return true;\n    }\n    if (other is AppInfo) {\n      return packageName == other.packageName;\n    }\n    return false;\n  }\n\n  @override\n  int get hashCode => packageName.hashCode;\n}\n"
  },
  {
    "path": "lib/native/native_method.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nclass NativeMethod {\n  static const MethodChannel _channel = MethodChannel('com.proxypin/method');\n\n  /// 检查本地网络（Wi-Fi 或以太网）是否可用 (仅限 iOS)。\n  ///\n  /// 返回 `true` 如果本地网络可用，否则返回 `false`。\n  static Future<bool> requestLocalNetworkAccess() async {\n    try {\n      final bool isAvailable = await _channel.invokeMethod('requestLocalNetwork');\n      logger.d(\"[NativeMethod] requestLocalNetworkAccess => $isAvailable\");\n      return isAvailable;\n    } on PlatformException catch (e) {\n      logger.e(\"[NativeMethod] requestLocalNetworkAccess error: '${e.message}'.\");\n      return false;\n    }\n  }\n\n  /// iOS: 检查给定 PEM 证书是否已安装到系统钥匙串\n  static Future<bool> isCaInstalled(String pem) async {\n    try {\n      final bool installed = await _channel.invokeMethod('isCaInstalled', {\"pem\": pem});\n      return installed;\n    } on PlatformException catch (e) {\n      logger.e(\"[NativeMethod] isCaInstalled error: ${e.message}\");\n      return false;\n    }\n  }\n\n  /// iOS: 基于 SSL 策略校验证书链（leaf + CA），仅当 CA 被系统信任时返回 true\n  static Future<bool> evaluateChainTrusted(String leafPem, String caPem, {String? host}) async {\n    try {\n      final bool trusted = await _channel.invokeMethod('evaluateChainTrusted', {\n        'leafPem': leafPem,\n        'caPem': caPem,\n        if (host != null) 'host': host,\n      });\n      return trusted;\n    } on PlatformException catch (e) {\n      logger.e(\"[NativeMethod] evaluateChainTrusted error: ${e.message}\");\n      return false;\n    }\n  }\n\n}"
  },
  {
    "path": "lib/native/pip.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/native/vpn.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/launch/launch.dart';\nimport 'package:proxypin/ui/mobile/mobile.dart';\nimport 'package:proxypin/utils/lang.dart';\n\n///画中画\nclass PictureInPicture {\n  static bool inPip = false;\n\n  static final MethodChannel _channel = const MethodChannel('com.proxy/pictureInPicture')\n    ..setMethodCallHandler((call) async {\n      logger.d(\"pictureInPicture MethodCallHandler ${call.method}\");\n      if (call.method == 'cleanSession') {\n        MobileApp.requestStateKey.currentState?.clean();\n      } else if (call.method == 'exitPictureInPictureMode') {\n        inPip = false;\n        Vpn.isRunning().then((value) {\n          Vpn.isVpnStarted = value;\n          SocketLaunch.startStatus.value = ValueWrap.of(value);\n        });\n      }\n\n      return Future.value();\n    });\n\n  ///进入画中画模式\n  static Future<bool> enterPictureInPictureMode(String host, int port,\n      {List<String>? appList, List<String>? disallowApps}) async {\n    final bool enterPictureInPictureMode = await _channel.invokeMethod('enterPictureInPictureMode',\n        {\"proxyHost\": host, \"proxyPort\": port, \"allowApps\": appList, \"disallowApps\": disallowApps});\n    inPip = true;\n\n    return enterPictureInPictureMode;\n  }\n\n  ///退出画中画模式\n  static Future<bool> exitPictureInPictureMode() async {\n    final bool exitPictureInPictureMode = await _channel.invokeMethod('exitPictureInPictureMode');\n    return exitPictureInPictureMode;\n  }\n\n  ///发送数据\n  static Future<bool> addData(String text) async {\n    if (Platform.isIOS && inPip) {\n      _channel.invokeMethod('addData', text.fixAutoLines());\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "lib/native/process_info.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/util/process_info.dart';\n\nclass ProcessInfoPlugin {\n  static const MethodChannel _methodChannel = MethodChannel('com.proxy/processInfo');\n\n  static Future<ProcessInfo?> getProcessByPort(String host, int port) {\n    return _methodChannel.invokeMethod<Map>('getProcessByPort', {\"host\": host, \"port\": port}).then((process) {\n      if (process == null) return null;\n\n      return ProcessInfo(process['packageName'], process['name'], process['packageName'],\n          os: Platform.operatingSystem,\n          icon: process['icon'],\n          remoteHost: process['remoteHost'],\n          remotePost: process['remotePort']);\n    });\n  }\n\n  static Future<HostAndPort?> getRemoteAddressByPort(int port) async {\n    if (!Platform.isAndroid) return null;\n\n    return _methodChannel.invokeMethod<Map>('getRemoteAddressByPort', {\"port\": port}).then((process) {\n      if (process == null) return null;\n      return HostAndPort.host(process['remoteHost'], process['remotePort']);\n    });\n  }\n}\n"
  },
  {
    "path": "lib/native/vpn.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nclass Vpn {\n  static const MethodChannel proxyVpnChannel = MethodChannel('com.proxy/proxyVpn');\n\n  static bool isVpnStarted = false; //vpn是否已经启动\n\n  static void startVpn(String host, int port, Configuration configuration, {bool? ipProxy = false}) {\n    List<String>? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : [];\n\n    List<String>? disallowApps;\n    if (appList.isEmpty) {\n      disallowApps = configuration.appBlacklist ?? [];\n    }\n\n    logger.d(\"Starting VPN with host: $host, port: $port,  proxyPassDomains: ${configuration.proxyPassDomains.split(';')}\");\n    proxyVpnChannel.invokeMethod(\"startVpn\", {\n      \"proxyHost\": host,\n      \"proxyPort\": port,\n      \"allowApps\": appList,\n      \"disallowApps\": disallowApps,\n      \"ipProxy\": ipProxy,\n      \"setSystemProxy\": configuration.enableSystemProxy,\n      \"proxyPassDomains\": configuration.proxyPassDomains.split(';'),\n    });\n    isVpnStarted = true;\n  }\n\n  static void stopVpn() {\n    proxyVpnChannel.invokeMethod(\"stopVpn\");\n    isVpnStarted = false;\n  }\n\n  //重启vpn\n  static void restartVpn(String host, int port, Configuration configuration, {bool ipProxy = false}) {\n    List<String>? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : [];\n\n    List<String>? disallowApps;\n    if (appList.isEmpty) {\n      disallowApps = configuration.appBlacklist ?? [];\n    }\n    proxyVpnChannel.invokeMethod(\"restartVpn\", {\n      \"proxyHost\": host,\n      \"proxyPort\": port,\n      \"allowApps\": appList,\n      \"disallowApps\": disallowApps,\n      \"ipProxy\": ipProxy,\n      \"setSystemProxy\": configuration.enableSystemProxy,\n      \"proxyPassDomains\": configuration.proxyPassDomains.split(';'),\n    });\n\n    isVpnStarted = true;\n  }\n\n  static Future<bool> isRunning() async {\n    return await proxyVpnChannel.invokeMethod(\"isRunning\");\n  }\n}\n"
  },
  {
    "path": "lib/network/bin/configuration.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/util/file_read.dart';\nimport 'package:proxypin/network/components/host_filter.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/system_proxy.dart';\nimport 'package:proxypin/utils/platform.dart';\n\nclass Configuration {\n  ///代理相关配置\n  int port = 9099;\n\n  //是否启用https抓包\n  bool enableSsl = Platforms.isMobile();\n\n  //是否设置系统代理\n  bool enableSystemProxy = true;\n\n  //代理忽略域名\n  String proxyPassDomains = SystemProxy.proxyPassDomains;\n\n  //enabled socks5 proxy\n  bool enableSocks5 = true;\n\n  //外部代理\n  ProxyInfo? externalProxy;\n\n  //白名单应用\n  List<String> appWhitelist = [];\n\n  //白名单应用是否启用\n  bool appWhitelistEnabled = true;\n\n  //应用黑名单\n  List<String>? appBlacklist;\n\n  //远程连接 不持久化保存\n  String? remoteHost;\n\n  bool enabledHttp2 = false; // 是否启用http2\n\n  //历史记录缓存时间\n  int historyCacheTime = 0;\n\n  //默认是否启动\n  bool startup = false;\n\n  Configuration._();\n\n  /// 单例\n  static Configuration? _instance;\n\n  static Future<Configuration> get instance async {\n    if (_instance == null) {\n      try {\n        var loadConfig = await _loadConfig();\n        _instance = Configuration.fromJson(loadConfig);\n      } catch (e) {\n        logger.e('初始化配置失败', error: e, stackTrace: StackTrace.current);\n        _instance = Configuration._();\n      }\n    }\n    return _instance!;\n  }\n\n  /// 加载配置\n  Configuration.fromJson(Map<String, dynamic> config) {\n    port = config['port'] ?? port;\n    enableSsl = config['enableSsl'] == true;\n    startup = config['startup'] ?? Platforms.isDesktop();\n    enableSystemProxy = config['enableSystemProxy'] ?? (config['enableDesktop'] ?? true);\n    enableSocks5 = config['enableSocks5'] ?? true;\n    enabledHttp2 = config['enabledHttp2'] ?? false;\n\n    proxyPassDomains = config['proxyPassDomains'] ?? SystemProxy.proxyPassDomains;\n    historyCacheTime = config['historyCacheTime'] ?? 0;\n    if (config['externalProxy'] != null) {\n      externalProxy = ProxyInfo.fromJson(config['externalProxy']);\n    }\n    appWhitelist = List<String>.from(config['appWhitelist'] ?? []);\n    appWhitelistEnabled = config['appWhitelistEnabled'] ?? true;\n    appBlacklist = config['appBlacklist'] == null ? null : List<String>.from(config['appBlacklist']);\n    HostFilter.whitelist.load(config['whitelist']);\n    HostFilter.blacklist.load(config['blacklist']);\n  }\n\n  /// 配置文件\n  static Future<File> configFile() async {\n    var separator = Platform.pathSeparator;\n    var home = await FileRead.homeDir();\n    return File(\"${home.path}${separator}config.cnf\");\n  }\n\n  /// 刷新配置文件\n  Future<void> flushConfig() async {\n    var file = await configFile();\n    var exists = await file.exists();\n    if (!exists) {\n      file = await file.create(recursive: true);\n    }\n    HostFilter.whitelist.toJson();\n    HostFilter.blacklist.toJson();\n    var json = jsonEncode(toJson());\n    logger.d('Refresh configuration file $runtimeType ${toJson()}');\n    file.writeAsString(json);\n  }\n\n  /// 加载配置文件\n  static Future<Map<String, dynamic>> _loadConfig() async {\n    var file = await configFile();\n    var exits = await file.exists();\n    if (!exits) {\n      return {};\n    }\n\n    Map<String, dynamic> config = jsonDecode(await file.readAsString());\n    logger.i('加载配置文件 [$file]');\n    return config;\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'port': port,\n      'enableSsl': enableSsl,\n      'startup': startup,\n      'enableSystemProxy': enableSystemProxy,\n      'enableSocks5': enableSocks5,\n      'proxyPassDomains': proxyPassDomains,\n      'externalProxy': externalProxy?.toJson(),\n      'appWhitelist': appWhitelist,\n      'appWhitelistEnabled': appWhitelistEnabled,\n      'appBlacklist': appBlacklist,\n      'historyCacheTime': historyCacheTime,\n      'enabledHttp2': enabledHttp2,\n      'whitelist': HostFilter.whitelist.toJson(),\n      'blacklist': HostFilter.blacklist.toJson(),\n    };\n  }\n}\n"
  },
  {
    "path": "lib/network/bin/listener.dart",
    "content": "import 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/websocket.dart';\n\n///请求和响应事件监听\nabstract class EventListener {\n  void onRequest(Channel channel, HttpRequest request);\n\n  void onResponse(ChannelContext channelContext, HttpResponse response);\n\n  void onMessage(Channel channel, HttpMessage message, WebSocketFrame frame) {}\n}\n\n\nclass CombinedEventListener extends EventListener {\n  final List<EventListener> listeners;\n\n  CombinedEventListener(this.listeners);\n\n  @override\n  void onRequest(Channel channel, HttpRequest request) {\n    for (var element in listeners) {\n      element.onRequest(channel, request);\n    }\n  }\n\n  @override\n  void onResponse(ChannelContext channelContext, HttpResponse response) {\n    for (var element in listeners) {\n      element.onResponse(channelContext, response);\n    }\n  }\n\n  @override\n  void onMessage(Channel channel, HttpMessage message, WebSocketFrame frame) {\n    for (var element in listeners) {\n      element.onMessage(channel, message, frame);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/network/bin/server.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:async';\nimport 'dart:io';\n\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/components/hosts.dart';\nimport 'package:proxypin/network/components/interceptor.dart';\nimport 'package:proxypin/network/components/report_server_interceptor.dart';\nimport 'package:proxypin/network/components/request_block.dart';\nimport 'package:proxypin/network/components/request_rewrite.dart';\nimport 'package:proxypin/network/components/script.dart';\nimport 'package:proxypin/network/handle/http_proxy_handle.dart';\nimport 'package:proxypin/network/util/crts.dart';\nimport 'package:proxypin/utils/platform.dart';\n\nimport '../components/request_map.dart';\nimport '../http/codec.dart';\nimport '../channel/network.dart';\nimport '../util/logger.dart';\nimport '../util/system_proxy.dart';\nimport 'listener.dart';\nimport 'package:proxypin/network/components/request_breakpoint.dart';\n\nFuture<void> main() async {\n  var configuration = await Configuration.instance;\n  ProxyServer(configuration).start();\n}\n\n/// 代理服务器\nclass ProxyServer {\n  static ProxyServer? current;\n\n  //socket服务\n  Server? server;\n\n  //请求事件监听\n  List<EventListener> listeners = [];\n\n  //配置\n  final Configuration configuration;\n\n  ProxyServer(this.configuration) {\n    current = this;\n  }\n\n  //是否启动\n  bool get isRunning => server?.isRunning ?? false;\n\n  ///是否启用https抓包\n  bool get enableSsl => configuration.enableSsl;\n\n  int get port => configuration.port;\n\n  set enableSsl(bool enableSsl) {\n    configuration.enableSsl = enableSsl;\n    if (server == null || server?.isRunning == false) {\n      return;\n    }\n\n    if (configuration.enableSystemProxy) {\n      SystemProxy.setSslProxyEnable(enableSsl, port);\n    }\n  }\n\n  /// 启动代理服务\n  Future<Server> start() async {\n    Server server = Server(configuration, listener: CombinedEventListener(listeners));\n\n    List<Interceptor> interceptors = [\n      Hosts(),\n      RequestMapInterceptor.instance,\n      RequestRewriteInterceptor.instance,\n      ScriptInterceptor(),\n      RequestBlockInterceptor(),\n      RequestBreakpointInterceptor.instance, // Register the interceptor\n      ReportServerInterceptor()\n    ];\n\n    interceptors.sort((a, b) => a.priority.compareTo(b.priority));\n\n    server.initChannel((channel) {\n      channel.dispatcher.handle(\n        HttpRequestCodec(),\n        HttpResponseCodec(),\n        HttpProxyChannelHandler(listener: CombinedEventListener(listeners), interceptors: interceptors),\n      );\n    });\n\n    return server.bind(port).then((serverSocket) {\n      logger.i(\"listen on $port\");\n      this.server = server;\n      if (configuration.enableSystemProxy) {\n        setSystemProxyEnable(true);\n      }\n\n      //初始化证书\n      CertificateManager.initCAConfig();\n      return server;\n    });\n  }\n\n  /// 停止代理服务\n  Future<Server?> stop() async {\n    if (!isRunning) {\n      return server;\n    }\n\n    if (configuration.enableSystemProxy) {\n      await setSystemProxyEnable(false);\n    }\n    logger.i(\"stop on $port\");\n    await server?.stop();\n    return server;\n  }\n\n  /// 设置系统代理\n  Future<void> setSystemProxyEnable(bool enable) async {\n    if (!Platforms.isDesktop()) {\n      return;\n    }\n\n    //关闭系统代理 恢复成外部代理地址\n    if (!enable && configuration.externalProxy?.enabled == true) {\n      await SystemProxy.setSystemProxy(configuration.externalProxy!.port!, enableSsl, configuration.proxyPassDomains);\n      return;\n    }\n\n    await SystemProxy.setSystemProxyEnable(port, enable, enableSsl, passDomains: configuration.proxyPassDomains);\n  }\n\n  /// 重启代理服务\n  Future<void> restart() async {\n    await stop().whenComplete(() => start());\n  }\n\n  ///检查是否监听端口 没有监听则启动\n  Future<void> retryBind() async {\n    try {\n      await Socket.connect('127.0.0.1', port, timeout: const Duration(milliseconds: 350));\n    } catch (e) {\n      logger.d('端口未被占用，尝试重新绑定 $port');\n      await restart();\n    }\n  }\n\n  ///添加监听器\n  void addListener(EventListener listener) {\n    listeners.add(listener);\n  }\n}\n"
  },
  {
    "path": "lib/network/channel/channel.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:async';\nimport 'dart:io';\nimport 'dart:math';\n\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/socket_address.dart';\n\nimport 'channel_dispatcher.dart';\n\n///处理I/O事件或截获I/O操作\n///[T] 读取的数据类型\n///@author wanghongen\nabstract class ChannelHandler<T> {\n  var log = logger;\n\n  ///连接建立\n  void channelActive(ChannelContext context, Channel channel) {}\n\n  ///读取数据事件\n  Future<void> channelRead(ChannelContext channelContext, Channel channel, T msg) async {}\n\n  ///连接断开\n  void channelInactive(ChannelContext channelContext, Channel channel) {\n    //log.i(\"[${channel.id}] close $channel\");\n  }\n\n  void exceptionCaught(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) {\n    HostAndPort? host = channelContext.host;\n    log.e(\"[${channel.id}] exceptionCaught $host $channel\", error: error, stackTrace: trace);\n    channel.close();\n  }\n}\n\n///与网络套接字或组件的连接，能够进行读、写、连接和绑定等I/O操作。\nclass Channel {\n  final int _id;\n  final ChannelDispatcher dispatcher = ChannelDispatcher();\n  Socket _socket;\n\n  //是否打开\n  bool isOpen = true;\n\n  //此通道连接到的远程地址\n  final InetSocketAddress remoteSocketAddress;\n\n  //是否写入中\n  bool isWriting = false;\n\n  Object? error; //异常\n  //是否使用代理\n  bool useProxy = false;\n\n  Channel(this._socket)\n      : _id = DateTime.now().millisecondsSinceEpoch + Random().nextInt(999999),\n        remoteSocketAddress = InetSocketAddress(_socket.remoteAddress, _socket.remotePort);\n\n  ///返回此channel的全局唯一标识符。\n  String get id => _id.toRadixString(36);\n\n  Socket get socket => _socket;\n\n  void serverSecureSocket(SecureSocket secureSocket, ChannelContext channelContext) {\n    _socket = secureSocket;\n    _socket.done.then((value) => isOpen = false);\n    dispatcher.listen(this, channelContext);\n  }\n\n  //向远程发起ssl连接\n  Future<SecureSocket> secureSocket(ChannelContext channelContext,\n      {String? host, List<String>? supportedProtocols}) async {\n    SecureSocket secureSocket = await SecureSocket.secure(socket,\n        host: host, supportedProtocols: supportedProtocols, onBadCertificate: (certificate) => true);\n\n    _socket = secureSocket;\n    _socket.done.then((value) => isOpen = false);\n    dispatcher.listen(this, channelContext);\n\n    return secureSocket;\n  }\n\n  Future<SecureSocket> startSecureSocket(ChannelContext channelContext,\n      {String? host, List<String>? supportedProtocols}) async {\n    SecureSocket secureSocket = await SecureSocket.secure(socket,\n        host: host, supportedProtocols: supportedProtocols, onBadCertificate: (certificate) => true);\n\n    _socket = secureSocket;\n    _socket.done.then((value) => isOpen = false);\n    return secureSocket;\n  }\n\n  void listen(ChannelContext channelContext) {\n    dispatcher.listen(this, channelContext);\n  }\n\n  String? get selectedProtocol => isSsl && isOpen ? (_socket as SecureSocket).selectedProtocol : null;\n\n  ///是否是ssl链接\n  bool get isSsl => _socket is SecureSocket;\n\n  Future<void> write(ChannelContext channelContext, Object obj) async {\n    var data = dispatcher.encoder.encode(channelContext, obj);\n    await writeBytes(data);\n  }\n\n  Future<void> writeBytes(List<int> bytes) async {\n    if (isClosed) {\n      logger.w(\"[$id] $remoteSocketAddress channel is closed\", stackTrace: StackTrace.current);\n    }\n\n    //只能有一个写入\n    int retry = 0;\n    while (isWriting && retry++ < 30) {\n      await Future.delayed(const Duration(milliseconds: 100));\n    }\n\n    if (isWriting) {\n      logger.d(\"[$id] write busy\");\n    }\n\n    isWriting = true;\n    try {\n      if (!isClosed) {\n        _socket.add(bytes);\n      }\n    } catch (e, t) {\n      if (e is StateError && e.message == \"StreamSink is closed\") {\n        logger.w(\"[$id] $remoteSocketAddress write error channel is closed $e\", stackTrace: t);\n      } else {\n        logger.e(\"[$id] write error\", error: e, stackTrace: t);\n      }\n    } finally {\n      isWriting = false;\n    }\n  }\n\n  ///写入并关闭此channel\n  Future<void> writeAndClose(ChannelContext channelContext, Object obj) async {\n    await write(channelContext, obj);\n    close();\n  }\n\n  ///关闭此channel\n  void close() async {\n    if (isClosed) {\n      return;\n    }\n\n    //写入中，延迟关闭\n    int retry = 0;\n    while (isWriting && retry++ < 10) {\n      await Future.delayed(const Duration(milliseconds: 150));\n    }\n    isOpen = false;\n    // if (!isWriting) {\n    //   await _socket.flush();\n    // }\n    await _socket.close();\n    // _socket.destroy();\n  }\n\n  ///返回此channel是否打开\n  bool get isClosed => !isOpen;\n\n  @override\n  String toString() {\n    return 'Channel($id $remoteSocketAddress';\n  }\n}\n"
  },
  {
    "path": "lib/network/channel/channel_context.dart",
    "content": "import 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/codec.dart';\nimport 'package:proxypin/network/http/h2/frame.dart';\nimport 'package:proxypin/network/http/h2/setting.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/attribute_keys.dart';\nimport 'package:proxypin/network/util/process_info.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nimport '../bin/listener.dart';\nimport 'network.dart';\n\n///\nclass ChannelContext {\n  final Map<String, Object> _attributes = {};\n\n  //和本地客户端的连接\n  Channel? clientChannel;\n\n  //和远程服务端的连接\n  Channel? serverChannel;\n\n  EventListener? listener;\n\n  //http2 stream\n  final Map<int, Pair<HttpRequest?, HttpResponse?>> _streams = {};\n  final Map<int, HeadersFrame> _streamDependency = {};\n\n  ChannelContext();\n\n  //创建服务端连接\n  Future<Channel> connectServerChannel(HostAndPort hostAndPort, ChannelHandler channelHandler) async {\n    serverChannel = await startConnect(hostAndPort, channelHandler, this);\n    putAttribute(clientChannel!.id, serverChannel);\n    putAttribute(serverChannel!.id, clientChannel);\n    return serverChannel!;\n  }\n\n  /// 建立连接\n  static Future<Channel> startConnect(\n      HostAndPort hostAndPort, ChannelHandler handler, ChannelContext channelContext) async {\n    var client = Client()..initChannel((channel) => channel.dispatcher.channelHandle(HttpClientCodec(), handler));\n\n    return client.connect(hostAndPort, channelContext);\n  }\n\n  T? getAttribute<T>(String key) {\n    if (!_attributes.containsKey(key)) {\n      return null;\n    }\n    return _attributes[key] as T;\n  }\n\n  void putAttribute(String key, Object? value) {\n    if (value == null) {\n      _attributes.remove(key);\n      return;\n    }\n    _attributes[key] = value;\n  }\n\n  HostAndPort? get host => getAttribute(AttributeKeys.host);\n\n  set host(HostAndPort? host) => putAttribute(AttributeKeys.host, host);\n\n  HttpRequest? get currentRequest => getAttribute(AttributeKeys.request);\n\n  set currentRequest(HttpRequest? request) => putAttribute(AttributeKeys.request, request);\n\n  set processInfo(ProcessInfo? processInfo) => putAttribute(AttributeKeys.processInfo, processInfo);\n\n  ProcessInfo? get processInfo => getAttribute(AttributeKeys.processInfo);\n\n  StreamSetting? setting;\n\n  HttpRequest? putStreamRequest(int streamId, HttpRequest request) {\n    var old = _streams[streamId]?.key;\n    _streams[streamId] = Pair(request, null);\n    return old;\n  }\n\n  void putStreamResponse(int streamId, HttpResponse response) {\n    var pair = _streams[streamId];\n    if (pair == null) {\n      pair = Pair(null, response);\n      _streams[streamId] = pair;\n    }\n\n    pair.key?.response = response;\n    response.request = pair.key;\n    pair.value = response;\n  }\n\n  HttpRequest? getStreamRequest(int streamId) {\n    return _streams[streamId]?.key;\n  }\n\n  HttpResponse? getStreamResponse(int streamId) {\n    return _streams[streamId]?.value;\n  }\n\n  void removeStream(int streamId) {\n    _streams.remove(streamId);\n  }\n\n  void put(int streamId, HeadersFrame frame) {\n    _streamDependency[streamId] = frame;\n  }\n\n  HeadersFrame? removeStreamDependency(int streamId) {\n    return _streamDependency.remove(streamId);\n  }\n\n  HeadersFrame? getStreamDependency(int streamId) {\n    return _streamDependency[streamId];\n  }\n\n  bool containsStreamDependency(int? streamId) {\n    if (streamId == null) return false;\n    return _streamDependency.containsKey(streamId);\n  }\n}\n"
  },
  {
    "path": "lib/network/channel/channel_dispatcher.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/handle/relay_handle.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/handle/websocket_handle.dart';\nimport 'package:proxypin/network/http/codec.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/network/util/attribute_keys.dart';\nimport 'package:proxypin/network/util/byte_buf.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/process_info.dart';\nimport 'package:proxypin/network/handle/sse_handle.dart';\n\nimport '../util/task_queue.dart';\n\nclass ChannelDispatcher extends ChannelHandler<Uint8List> {\n  late Decoder decoder;\n  late Encoder encoder;\n  late ChannelHandler handler;\n\n  final ByteBuf buffer = ByteBuf();\n\n  //h2 stream dependency Sequential exec\n  SequentialTaskQueue taskQueue = SequentialTaskQueue();\n\n  void handle(Decoder decoder, Encoder encoder, ChannelHandler handler) {\n    this.encoder = encoder;\n    this.decoder = decoder;\n    this.handler = handler;\n  }\n\n  void channelHandle(Codec codec, ChannelHandler handler) {\n    handle(codec, codec, handler);\n  }\n\n  /// 监听\n  void listen(Channel channel, ChannelContext channelContext) {\n    buffer.clear();\n    channel.socket.done.onError((error, StackTrace trace) {\n      logger.e('[${channelContext.clientChannel?.id}] secureSocket done error', error: error, stackTrace: trace);\n      channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace);\n      return null;\n    });\n    channel.socket.listen((data) => channel.dispatcher.channelRead(channelContext, channel, data),\n        onError: (error, trace) => channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace),\n        onDone: () => channel.dispatcher.channelInactive(channelContext, channel));\n  }\n\n  @override\n  void channelActive(ChannelContext context, Channel channel) {\n    handler.channelActive(context, channel);\n  }\n\n  ///远程转发请求\n  Future<void> remoteForward(ChannelContext channelContext, HostAndPort remote) async {\n    var clientChannel = channelContext.clientChannel!;\n    Channel? remoteChannel =\n        channelContext.serverChannel ?? await channelContext.connectServerChannel(remote, RelayHandler(clientChannel));\n    ProxyInfo? proxyInfo = channelContext.getAttribute(AttributeKeys.proxyInfo);\n    if (clientChannel.isSsl && !remoteChannel.isSsl) {\n      //代理认证\n      if (proxyInfo?.isAuthenticated == true) {\n        await HttpClients.connectRequest(channelContext, remote, remoteChannel, proxyInfo: proxyInfo);\n      }\n\n      await remoteChannel.secureSocket(channelContext, host: channelContext.getAttribute(AttributeKeys.domain));\n    }\n\n    relay(channelContext, clientChannel, remoteChannel);\n  }\n\n  /// 转发请求\n  void relay(ChannelContext channelContext, Channel clientChannel, Channel remoteChannel) {\n    var rawCodec = RawCodec();\n    clientChannel.dispatcher.channelHandle(rawCodec, RelayHandler(remoteChannel));\n    remoteChannel.dispatcher.channelHandle(rawCodec, RelayHandler(clientChannel));\n\n    var body = buffer.bytes;\n    buffer.clear();\n    handler.channelRead(channelContext, clientChannel, body);\n  }\n\n  @override\n  Future<void> channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async {\n    //手机扫码连接转发远程\n    HostAndPort? remote = channelContext.getAttribute(AttributeKeys.remote);\n    buffer.add(msg);\n\n    try {\n      if (remote != null) {\n        await remoteForward(channelContext, remote);\n        return;\n      }\n\n      Channel? remoteChannel = channelContext.getAttribute(channel.id);\n\n      //大body 不解析直接转发\n      if (buffer.length > Codec.maxBodyLength && handler is! RelayHandler && remoteChannel != null) {\n        logger.w(\"[$channel] forward large body\");\n        relay(channelContext, channel, remoteChannel);\n        return;\n      }\n\n      var decodeResult = decoder.decode(channelContext, buffer);\n\n      //If the body does not support parsing, forward directly\n      if (decodeResult.supportedParse == false) {\n        notSupportedForward(channelContext, channel, decodeResult);\n        return;\n      }\n\n      if (decodeResult.forward != null) {\n        buffer.clearRead();\n\n        if (remoteChannel != null) {\n          await remoteChannel.writeBytes(decodeResult.forward!);\n        } else {\n          logger.w(\"[$channel] forward remoteChannel is null\");\n        }\n\n        if (decodeResult.data == null) {\n          return;\n        }\n      }\n\n      if (!decodeResult.isDone) {\n        return;\n      }\n\n      var length = buffer.length;\n      buffer.clearRead();\n\n      var data = decodeResult.data;\n      if (data is HttpMessage) {\n        data.packageSize ??= length;\n        data.remoteHost = channel.remoteSocketAddress.host;\n        data.remotePort = channel.remoteSocketAddress.port;\n      }\n\n      if (data is HttpRequest) {\n        channelContext.currentRequest = data;\n        data.hostAndPort ??= channelContext.host ?? getHostAndPort(data, ssl: channel.isSsl);\n        if (data.headers.host != null && data.headers.host?.contains(\":\") == false) {\n          data.hostAndPort?.host = data.headers.host!;\n        }\n\n        data.processInfo ??= await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, data.remoteDomain()!);\n      }\n\n      if (data is HttpResponse) {\n        data.requestId = channelContext.currentRequest?.requestId ?? data.requestId;\n        data.request ??= channelContext.currentRequest;\n      }\n\n      //websocket协议\n      if (data is HttpResponse && data.isWebSocket && remoteChannel != null) {\n        onWebSocketHandle(channelContext, channel, data);\n        return;\n      }\n\n      if (data is HttpMessage && channelContext.containsStreamDependency(data.streamId)) {\n        taskQueue.add(data.streamId!, channelContext.getStreamDependency(data.streamId!)?.streamDependency,\n            () => handler.channelRead(channelContext, channel, data),\n            onError: (error, stackTrace) => onError(channelContext, channel, error, trace: stackTrace));\n      } else {\n        await handler.channelRead(channelContext, channel, data!);\n      }\n    } catch (error, trace) {\n      onError(channelContext, channel, error, trace: trace);\n    }\n  }\n\n  void onError(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) {\n    logger.e(\n        \"[${channelContext.clientChannel?.id}] channelRead error isSsl:${channel.isSsl} client: ${channelContext.clientChannel?.selectedProtocol} server: ${channelContext.serverChannel?.selectedProtocol} ${String.fromCharCodes(buffer.bytes)}\",\n        error: error,\n        stackTrace: trace);\n    buffer.clear();\n    exceptionCaught(channelContext, channel, error, trace: trace);\n  }\n\n  /// websocket 处理\n  void onWebSocketHandle(ChannelContext channelContext, Channel channel, HttpResponse data) {\n    Channel remoteChannel = channelContext.getAttribute(channel.id);\n\n    data.request?.response = data;\n    channelContext.host =\n        channelContext.host?.copyWith(scheme: channel.isSsl ? HostAndPort.wssScheme : HostAndPort.wsScheme);\n    channelContext.currentRequest?.hostAndPort = channelContext.host;\n\n    logger.d(\"webSocket ${data.request?.hostAndPort}\");\n    remoteChannel.write(channelContext, data);\n\n    channelContext.listener?.onResponse(channelContext, data);\n\n    var rawCodec = RawCodec();\n    channel.dispatcher.channelHandle(rawCodec, WebSocketChannelHandler(remoteChannel, data));\n    remoteChannel.dispatcher.channelHandle(rawCodec, WebSocketChannelHandler(channel, data.request!));\n  }\n\n  /// SSE 处理 (text/event-stream)\n  void onSseHandle(ChannelContext channelContext, Channel channel, HttpResponse response, List<int>? initialBody) {\n    Channel remoteChannel = channelContext.getAttribute(channel.id);\n    channelContext.currentRequest?.response = response;\n    response.request ??= channelContext.currentRequest;\n    channelContext.listener?.onResponse(channelContext, response);\n\n    remoteChannel.write(channelContext, response);\n\n    // Switch to raw streaming: server->client uses SseChannelHandler; client->server just relays\n    var rawCodec = RawCodec();\n    channel.dispatcher.channelHandle(rawCodec, SseChannelHandler(remoteChannel, response));\n    remoteChannel.dispatcher.channelHandle(rawCodec, RelayHandler(channel));\n\n    // Flush any initial body bytes that were already read\n    if (initialBody != null && initialBody.isNotEmpty) {\n      // Place existing buffered bytes and let handler consume\n      buffer.add(initialBody);\n      var body = buffer.bytes;\n      buffer.clear();\n      handler.channelRead(channelContext, channel, body);\n    }\n  }\n\n  void notSupportedForward(ChannelContext channelContext, Channel channel, DecoderResult decodeResult) {\n    Channel? remoteChannel = channelContext.getAttribute(channel.id);\n\n    // If this is an SSE response, switch to SSE streaming mode instead of generic relay\n    if (decodeResult.data is HttpResponse) {\n      var response = decodeResult.data as HttpResponse;\n      if (response.headers.contentType.toLowerCase().startsWith('text/event-stream') && remoteChannel != null) {\n        logger.d(\"[$channel] switch to SSE streaming\");\n        onSseHandle(channelContext, channel, response, decodeResult.forward);\n        return;\n      }\n    }\n\n    // Fallback: generic relay for unsupported body types\n    buffer.add(decodeResult.forward ?? []);\n    relay(channelContext, channel, remoteChannel!);\n\n    if (decodeResult.data is HttpResponse) {\n      var response = decodeResult.data as HttpResponse;\n      logger.w(\"[$channel] not supported parse ${response.headers.contentType}\");\n      response.request ??= channelContext.currentRequest;\n      channelContext.currentRequest?.response = response;\n      channelContext.listener?.onResponse(channelContext, response);\n    }\n  }\n\n  @override\n  exceptionCaught(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) {\n    handler.exceptionCaught(channelContext, channel, error, trace: trace);\n  }\n\n  @override\n  channelInactive(ChannelContext channelContext, Channel channel) async {\n    await taskQueue.waitForAll();\n    channel.isOpen = false;\n    handler.channelInactive(channelContext, channel);\n  }\n}\n\nclass RawCodec extends Codec<Uint8List, List<int>> {\n  @override\n  DecoderResult<Uint8List> decode(ChannelContext channelContext, ByteBuf byteBuf, {bool resolveBody = true}) {\n    var decoderResult = DecoderResult<Uint8List>()..data = byteBuf.readAvailableBytes();\n    return decoderResult;\n  }\n\n  @override\n  List<int> encode(ChannelContext channelContext, dynamic data) {\n    return data as List<int>;\n  }\n}\n\nabstract interface class ChannelInitializer {\n  void initChannel(Channel channel);\n}\n"
  },
  {
    "path": "lib/network/channel/host_port.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\n\n/// 获取主机和端口\nHostAndPort getHostAndPort(HttpRequest request, {bool? ssl}) {\n  String requestUri = request.uri;\n  //有些请求直接是路径 /xxx, 从header取host\n  if (request.uri.startsWith(\"/\")) {\n    requestUri = request.headers.get(HttpHeaders.HOST)!;\n  }\n  return HostAndPort.of(requestUri, ssl: ssl);\n}\n\nclass HostAndPort {\n  static final ipv6Pattern = r'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|'\n      r'([0-9a-fA-F]{1,4}:){1,7}:|'\n      r'([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|'\n      r'([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|'\n      r'([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|'\n      r'([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|'\n      r'([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|'\n      r'[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|'\n      r':((:[0-9a-fA-F]{1,4}){1,7}|:)|'\n      r'fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|'\n      r'::(ffff(:0{1,4}){0,1}:){0,1}'\n      r'(([0-9]{1,3}\\.){3,3}[0-9]{1,3})|'\n      r'([0-9a-fA-F]{1,4}:){1,4}:'\n      r'(([0-9]{1,3}\\.){3,3}[0-9]{1,3}))$';\n\n  static final ipV6RegExp = RegExp(ipv6Pattern);\n\n  static const String httpScheme = \"http://\";\n  static const String httpsScheme = \"https://\";\n  static const String wsScheme = \"ws://\";\n  static const String wssScheme = \"wss://\";\n\n  static const schemes = [httpsScheme, httpScheme, wssScheme, wsScheme];\n\n  String scheme;\n  String host;\n  final int port;\n  bool? _ipv6;\n\n  bool get isIPv6 {\n    _ipv6 ??= _isIPv6(host);\n    return _ipv6!;\n  }\n\n  static bool _isIPv6(String address) {\n    return ipV6RegExp.hasMatch(address);\n  }\n\n  HostAndPort(this.scheme, this.host, this.port, {bool? ipv6}) : _ipv6 = ipv6;\n\n  factory HostAndPort.host(String host, int port, {String? scheme}) {\n    return HostAndPort(scheme ?? (port == 443 ? httpsScheme : httpScheme), host, port);\n  }\n\n  /// 是否是url\n  static bool startsWithScheme(String url) {\n    return schemes.any((scheme) => url.startsWith(scheme));\n  }\n\n  bool isSsl() {\n    return httpsScheme.startsWith(scheme);\n  }\n\n  /// 根据url构建\n  static HostAndPort of(String url, {bool? ssl}) {\n    String domain = url;\n    String? scheme;\n    //域名格式 直接解析\n    if (startsWithScheme(url)) {\n      try {\n        Uri uri = Uri.parse(url);\n        return HostAndPort('${uri.scheme}://', uri.host, uri.port);\n      } catch (e) {\n        //httpScheme\n        scheme = schemes.firstWhere((element) => url.startsWith(element), orElse: () => httpScheme);\n        domain = url.substring(scheme.length).split(\"/\")[0];\n      }\n\n      //说明支持ipv6\n      if (domain.startsWith('[') && domain.endsWith(']')) {\n        return HostAndPort(scheme, domain, scheme == httpScheme ? 80 : 443, ipv6: true);\n      }\n    }\n\n    //ip格式 host:port\n    var indexOf = domain.lastIndexOf(':');\n    String host = domain.substring(0, indexOf == -1 ? domain.length : indexOf);\n    String? port = indexOf == -1 ? null : domain.substring(indexOf + 1, domain.length);\n    bool? ipv6 = host.startsWith('[') && host.endsWith(']') ? true : null;\n\n    if (port != null) {\n      bool isSsl = port == \"443\" || ssl == true;\n      scheme ??= isSsl ? httpsScheme : httpScheme;\n      return HostAndPort(scheme, host, int.parse(port), ipv6: ipv6);\n    }\n    scheme ??= (ssl == true ? httpsScheme : httpScheme);\n    return HostAndPort(scheme, host, scheme == httpScheme ? 80 : 443, ipv6: ipv6);\n  }\n\n  String get domain {\n    String host = this.host;\n    if (isIPv6 && !host.startsWith('[') && !host.endsWith(']')) {\n      host = '[$host]';\n    }\n    return '$scheme$host${(port == 80 || port == 443) ? \"\" : \":$port\"}';\n  }\n\n  HostAndPort copyWith({String? scheme, String? host, int? port}) {\n    return HostAndPort(scheme ?? this.scheme, host ?? this.host, port ?? this.port);\n  }\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is HostAndPort &&\n          runtimeType == other.runtimeType &&\n          scheme == other.scheme &&\n          host == other.host &&\n          port == other.port;\n\n  @override\n  int get hashCode => scheme.hashCode ^ host.hashCode ^ port.hashCode;\n\n  @override\n  String toString() {\n    return domain;\n  }\n}\n\n/// 代理信息\nclass ProxyInfo {\n  bool enabled = false;\n\n  //是否展示抓包\n  bool capturePacket = true;\n  String host = '127.0.0.1';\n  int? port;\n\n  //authorization\n  String? username;\n  String? password;\n\n  ProxyInfo();\n\n  ProxyInfo.of(this.host, this.port) : enabled = true;\n\n  bool get isAuthenticated => username?.isNotEmpty == true;\n\n  ProxyInfo.fromJson(Map<String, dynamic> json) {\n    enabled = json['enabled'] == true;\n    capturePacket = json['capturePacket'] ?? true;\n    host = json['host'];\n    port = json['port'];\n    username = json['username'];\n    password = json['password'];\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'enabled': enabled,\n      'capturePacket': capturePacket,\n      'host': host,\n      'port': port,\n      'username': username,\n      'password': password,\n    };\n  }\n\n  @override\n  String toString() {\n    return 'ProxyInfo{enabled: $enabled, capturePacket: $capturePacket, host: $host, port: $port, username: $username, password: $password}';\n  }\n}\n"
  },
  {
    "path": "lib/network/channel/network.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:async';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/native/process_info.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/channel/channel_dispatcher.dart';\nimport 'package:proxypin/network/components/host_filter.dart';\nimport 'package:proxypin/network/handle/relay_handle.dart';\nimport 'package:proxypin/network/socks/socks5.dart';\nimport 'package:proxypin/network/util/attribute_keys.dart';\nimport 'package:proxypin/network/util/crts.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/process_info.dart';\nimport 'package:proxypin/network/util/tls.dart';\n\nimport '../bin/listener.dart';\nimport 'host_port.dart';\n\nabstract class Network {\n  late Function _channelInitializer;\n\n  Network initChannel(void Function(Channel channel) initializer) {\n    _channelInitializer = initializer;\n    return this;\n  }\n\n  Channel listen(Channel channel, ChannelContext channelContext) {\n    _channelInitializer.call(channel);\n    channel.dispatcher.channelActive(channelContext, channel);\n\n    channel.socket.listen((data) => onEvent(data, channelContext, channel),\n        onError: (error, StackTrace trace) =>\n            channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace),\n        onDone: () => channel.dispatcher.channelInactive(channelContext, channel));\n\n    channel.socket.done.onError((error, StackTrace trace) {\n      logger.e('[${channelContext.clientChannel?.id}] socket done error', error: error, stackTrace: trace);\n      channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace);\n    });\n    return channel;\n  }\n\n  Future<void> onEvent(Uint8List data, ChannelContext channelContext, Channel channel);\n\n  /// 转发请求\n  void relay(Channel clientChannel, Channel remoteChannel) {\n    var rawCodec = RawCodec();\n    clientChannel.dispatcher.channelHandle(rawCodec, RelayHandler(remoteChannel));\n    remoteChannel.dispatcher.channelHandle(rawCodec, RelayHandler(clientChannel));\n  }\n}\n\nclass Server extends Network {\n  Configuration configuration;\n\n  late ServerSocket serverSocket;\n  bool isRunning = false;\n  EventListener? listener;\n  StreamSubscription? serverSubscription;\n  final List<Channel> _connections = [];\n  Timer? _connectionCleanupTimer;\n\n  Server(this.configuration, {this.listener});\n\n  Future<ServerSocket> bind(int port) async {\n    serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, port);\n    serverSubscription = serverSocket.listen((socket) {\n      var channel = Channel(socket);\n      _connections.add(channel);\n\n      socket.done.whenComplete(() => _connections.remove(channel));\n\n      ChannelContext channelContext = ChannelContext();\n      channelContext.clientChannel = channel;\n      channelContext.listener = listener;\n      listen(channel, channelContext);\n    });\n    isRunning = true;\n    _connectionCleanupTimer = Timer.periodic(const Duration(seconds: 120), (timer) {\n      if (!isRunning) {\n        timer.cancel();\n        _connectionCleanupTimer = null;\n        return;\n      }\n      cleanupConnections();\n    });\n    return serverSocket;\n  }\n\n  Future<ServerSocket> stop() async {\n    if (!isRunning) return serverSocket;\n    isRunning = false;\n    for (var channel in _connections) {\n      if (channel.isClosed) continue;\n      try {\n        logger.d('Closing socket: ${channel.remoteSocketAddress.host}:${channel.remoteSocketAddress.port}');\n        channel.close();\n      } catch (e) {\n        logger.e('Error closing socket: $e');\n      }\n    }\n    _connections.clear();\n    //关闭监听\n    serverSubscription?.cancel();\n    serverSubscription = null;\n    await serverSocket.close();\n    _connectionCleanupTimer?.cancel();\n    _connectionCleanupTimer = null;\n    return serverSocket;\n  }\n\n  void cleanupConnections() {\n    _connections.removeWhere((channel) {\n      if (channel.isClosed) {\n        logger.i('Cleaning up closed channel: ${channel.remoteSocketAddress.host}:${channel.remoteSocketAddress.port}');\n        return true;\n      }\n      return false;\n    });\n  }\n\n  @override\n  Future<void> onEvent(Uint8List data, ChannelContext channelContext, Channel channel) async {\n    //手机扫码转发远程地址\n    if (configuration.remoteHost != null) {\n      channelContext.putAttribute(AttributeKeys.remote, HostAndPort.of(configuration.remoteHost!));\n    }\n\n    //外部代理信息\n    if (configuration.externalProxy?.enabled == true) {\n      ProxyInfo externalProxy = configuration.externalProxy!;\n      channelContext.putAttribute(AttributeKeys.proxyInfo, externalProxy);\n\n      if (externalProxy.capturePacket == false) {\n        //不抓包直接转发\n        channelContext.putAttribute(AttributeKeys.remote, HostAndPort.host(externalProxy.host, externalProxy.port!));\n      }\n    }\n\n    HostAndPort? hostAndPort = channelContext.host;\n\n    //黑名单 或 没开启https 直接转发\n    if ((HostFilter.filter(hostAndPort?.host)) || (hostAndPort?.isSsl() == true && configuration.enableSsl == false)) {\n      var remoteChannel = channelContext.serverChannel ??\n          await channelContext.connectServerChannel(hostAndPort!, RelayHandler(channel));\n      relay(channel, remoteChannel);\n      channel.dispatcher.channelRead(channelContext, channel, data);\n      return;\n    }\n\n    //ssl握手\n    if (hostAndPort?.isSsl() == true || TLS.isTLSClientHello(data)) {\n      ssl(channelContext, channel, data);\n      return;\n    }\n\n    //socks5\n    if (configuration.enableSocks5 && Socks5.isSocks5(data) && channel.dispatcher.handler is! SocksServerHandler) {\n      channel.dispatcher.channelHandle(RawCodec(),\n          SocksServerHandler(channel.dispatcher.decoder, channel.dispatcher.encoder, channel.dispatcher.handler));\n    }\n\n    channel.dispatcher.channelRead(channelContext, channel, data);\n  }\n\n  /// ssl握手\n  void ssl(ChannelContext channelContext, Channel channel, Uint8List data) async {\n    var hostAndPort = channelContext.host;\n    try {\n      String? serviceName = TLS.getDomain(data) ?? hostAndPort?.host;\n      bool isHttp = true;\n\n      if (hostAndPort == null) {\n        var domain = serviceName;\n        var port = 443;\n\n        if (domain == null) {\n          var remote = await ProcessInfoPlugin.getRemoteAddressByPort(channel.remoteSocketAddress.port);\n          domain = remote?.host;\n          port = remote?.port ?? port;\n          serviceName = domain;\n\n          // DNS over HTTPS\n          if (remote?.port == 853 && TLS.supportProtocols(data)?.contains(\"http/1.1\") == false) {\n            isHttp = false;\n          }\n        }\n\n        hostAndPort = HostAndPort.host(domain!, port, scheme: HostAndPort.httpsScheme);\n      }\n\n      hostAndPort.scheme = HostAndPort.httpsScheme;\n      channelContext.putAttribute(AttributeKeys.domain, hostAndPort.host);\n\n      Channel? remoteChannel = channelContext.serverChannel;\n\n      if (!isHttp || HostFilter.filter(hostAndPort.host) || !configuration.enableSsl) {\n        remoteChannel = remoteChannel ?? await channelContext.connectServerChannel(hostAndPort, RelayHandler(channel));\n        relay(channel, remoteChannel);\n        channel.dispatcher.channelRead(channelContext, channel, data);\n        return;\n      }\n\n      if (remoteChannel != null && !remoteChannel.isSsl) {\n        var supportProtocols = configuration.enabledHttp2 ? TLS.supportProtocols(data) : ['http/1.1'];\n        await remoteChannel.startSecureSocket(channelContext, host: serviceName, supportedProtocols: supportProtocols);\n      }\n\n      //ssl自签证书\n      var certificate = await CertificateManager.getCertificateContext(serviceName!);\n      var selectedProtocol = remoteChannel?.selectedProtocol;\n\n      var supportedProtocols = selectedProtocol != null ? [selectedProtocol] : ['http/1.1'];\n\n      certificate.setAlpnProtocols(supportedProtocols, true);\n\n      //处理客户端ssl握手\n      var secureSocket = await SecureSocket.secureServer(channel.socket, certificate,\n          bufferedData: data, supportedProtocols: supportedProtocols);\n\n      channel.serverSecureSocket(secureSocket, channelContext);\n      remoteChannel?.listen(channelContext);\n\n      if (selectedProtocol != secureSocket.selectedProtocol) {\n        logger.i(\n            '[${channelContext.clientChannel?.id}] $hostAndPort ssl handshake done, clientSelectedProtocol: ${secureSocket.selectedProtocol}, serverSelectedProtocols: $supportedProtocols');\n      }\n    } catch (error, trace) {\n      logger.e('[${channelContext.clientChannel?.id}] $hostAndPort ssl error', error: error, stackTrace: trace);\n      try {\n        channelContext.processInfo ??=\n            await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, hostAndPort?.domain ?? 'unknown');\n      } catch (ignore) {\n        /*ignore*/\n      }\n\n      channelContext.host ??= hostAndPort;\n      channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace);\n    }\n  }\n}\n\nclass Client extends Network {\n  Future<Channel> connect(HostAndPort hostAndPort, ChannelContext channelContext,\n      {Duration timeout = const Duration(seconds: 3)}) async {\n    String host = hostAndPort.host;\n    //说明支持ipv6\n    // if (host.startsWith(\"[\") && host.endsWith(']')) {\n    //   host = host.substring(1, host.length - 1);\n    // }\n\n    // logger.d('Connecting to $host:${hostAndPort.port}');\n    return Socket.connect(host, hostAndPort.port, timeout: timeout).then((socket) {\n      if (socket.address.type != InternetAddressType.unix) {\n        socket.setOption(SocketOption.tcpNoDelay, true);\n      }\n      var channel = Channel(socket);\n      channelContext.serverChannel = channel;\n      return listen(channel, channelContext);\n    });\n  }\n\n  /// ssl连接\n  Future<Channel> secureConnect(HostAndPort hostAndPort, ChannelContext channelContext) async {\n    return SecureSocket.connect(hostAndPort.host, hostAndPort.port,\n        timeout: const Duration(seconds: 3), onBadCertificate: (certificate) => true).then((socket) {\n      var channel = Channel(socket);\n      channelContext.serverChannel = channel;\n      return listen(channel, channelContext);\n    });\n  }\n\n  @override\n  Future<void> onEvent(Uint8List data, ChannelContext channelContext, Channel channel) async {\n    channel.dispatcher.channelRead(channelContext, channel, data);\n  }\n}\n"
  },
  {
    "path": "lib/network/components/host_filter.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nvoid main() {\n  print(HostFilter.filter(\"stackoverflow.com\"));\n}\n\n/// @author wanghongen\n/// 2023/7/26\nclass HostFilter {\n  /// 白名单\n  static final Whites whitelist = Whites();\n\n  /// 黑名单\n  static final Blacks blacklist = Blacks();\n\n  /// 是否过滤\n  static bool filter(String? host) {\n    if (host == null) {\n      return false;\n    }\n\n    //如果白名单不为空，不在白名单里都是黑名单\n    if (whitelist.enabled) {\n      return whitelist.list.every((element) => !element.hasMatch(host));\n    }\n\n    if (blacklist.enabled) {\n      return blacklist.list.any((element) => element.hasMatch(host));\n    }\n    return false;\n  }\n}\n\n///\nabstract class HostList {\n  /// 列表\n  final List<RegExp> list = [];\n  bool enabled = false;\n\n  ///加载配置\n  void load(Map<String, dynamic>? map) {\n    if (map == null) {\n      return;\n    }\n    List? list = map['list'];\n    this.list.clear();\n    list?.forEach((element) {\n      this.list.add(RegExp(element));\n    });\n    enabled = map['enabled'] == true;\n  }\n\n  void add(String reg) {\n    var regExp = RegExp(reg.replaceAll(\"*\", \".*\"));\n    list.removeWhere((element) => element.pattern == regExp.pattern);\n    list.add(regExp);\n  }\n\n  void remove(String reg) {\n    list.removeWhere((element) => element.pattern == reg.replaceAll(\"*\", \".*\"));\n  }\n\n  void removeIndex(List<int> index) {\n    for (var element in index) {\n      list.removeAt(element);\n    }\n  }\n\n  // json序列化\n  Map<String, dynamic> toJson() {\n    return {\n      'list': list.map((e) => e.pattern).toList(),\n      'enabled': enabled,\n    };\n  }\n}\n\n///白名单\nclass Whites extends HostList {}\n\n///黑名单\nclass Blacks extends HostList {\n  Blacks() {\n    enabled = true;\n    list.add(RegExp(\".*.apple.com\"));\n    list.add(RegExp(\".*.icloud.com\"));\n  }\n}\n"
  },
  {
    "path": "lib/network/components/hosts.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'package:proxypin/network/components/manager/hosts_manager.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nimport 'interceptor.dart';\n\n/// Hosts interceptor\n/// @author wanghongen\nclass Hosts extends Interceptor {\n  Future<HostsManager> get hostsManager async => await HostsManager.instance;\n\n  @override\n  int get priority => -1000;\n\n  @override\n  Future<HostAndPort> preConnect(HostAndPort hostAndPort) async {\n    var host = hostAndPort.host;\n    var hostsItem = await hostsManager.then((it) => it.getHosts(host));\n    if (hostsItem != null) {\n      logger.d('Hosts: $host -> ${hostsItem.toAddress}');\n      return hostAndPort.copyWith(host: hostsItem.toAddress);\n    }\n    return hostAndPort;\n  }\n}\n"
  },
  {
    "path": "lib/network/components/interceptor.dart",
    "content": "import 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\n\n/// A Interceptor that can intercept and modify the request and response.\n/// @author Hongen Wang\nabstract class Interceptor {\n  /// The priority of the interceptor.\n  int get priority => 0;\n\n  Future<HostAndPort> preConnect(HostAndPort hostAndPort) async {\n    return hostAndPort;\n  }\n\n  /// Called before the request is sent to the server.\n  Future<HttpResponse?> execute(HttpRequest request) async {\n    return null;\n  }\n\n  /// Called before the request is sent to the server.\n  Future<HttpRequest?> onRequest(HttpRequest request) async {\n    return request;\n  }\n\n  /// Called after the response is received from the server.\n  Future<HttpResponse?> onResponse(HttpRequest request, HttpResponse response) async {\n    return response;\n  }\n\n  Future<void> onError(HttpRequest? request, dynamic error, StackTrace? stackTrace) async {\n    return;\n  }\n}\n"
  },
  {
    "path": "lib/network/components/js/file.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'dart:io';\n\nimport 'package:flutter_js/flutter_js.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\n/// FileBridge for file operation\n/// @Author: Hongen Wang\nclass FileBridge {\n  static const String code = '''\n    function getApplicationSupportDirectory() {\n      return sendMessage('getApplicationSupportDirectory', JSON.stringify(''));\n    }\n    \n    function File(path) {\n      return {\n        path: path,\n        readAsString: function() {\n          return sendMessage('file.readAsString', JSON.stringify(this.path));\n        },\n        readAsStringSync: function() {\n          return sendMessage('file.readAsStringSync', JSON.stringify(this.path));\n        },\n        readAsBytes: function() {\n          return sendMessage('file.readAsBytes', JSON.stringify(this.path));\n        },\n        readAsBytesSync: function() {\n          return sendMessage('file.readAsBytesSync', JSON.stringify(this.path));\n        },\n        writeAsString: function(content, append) {\n          return sendMessage('file.writeAsString', JSON.stringify({path: this.path, content:content, append: append}));\n        },\n        writeAsStringSync: function(content, append) {\n          return sendMessage('file.writeAsStringSync', JSON.stringify({path: this.path, content: content, append: append}));\n        },\n        writeAsBytes: function(bytes, append) {\n          return sendMessage('file.writeAsBytes', JSON.stringify({path: this.path, bytes: bytes, append: append}));\n        },\n        writeAsBytesSync: function(bytes, append) {\n          return sendMessage('file.writeAsBytesSync', JSON.stringify({path: this.path, bytes: bytes, append: append}));\n        },\n        length: function() {\n          return sendMessage('file.length', JSON.stringify(this.path));\n        },\n        lengthSync: function() {\n          return sendMessage('file.lengthSync', JSON.stringify(this.path));\n        },\n        delete: function() {\n          return sendMessage('file.delete', JSON.stringify(this.path));\n        },\n        deleteSync: function() {\n          return sendMessage('file.deleteSync', JSON.stringify(this.path));\n        },        \n        exists: function() {\n          return sendMessage('file.exists', JSON.stringify(this.path));\n        },\n        existsSync: function() {\n          return sendMessage('file.existsSync', JSON.stringify(this.path));\n        },\n        create: function(recursive, exclusive) {\n          return sendMessage('file.create', JSON.stringify({path: this.path, recursive: recursive, exclusive: exclusive}));\n        },\n        createSync: function(recursive, exclusive) {\n          return sendMessage('file.createSync', JSON.stringify({path: this.path, recursive: recursive, exclusive: exclusive}));\n        },\n        rename: function(newPath) {\n          return sendMessage('file.rename', JSON.stringify(this.path, newPath));\n        }\n      };\n    }\n  ''';\n\n  ///register file operation\n  static void registerFile(JavascriptRuntime flutterJs) {\n    var channels = JavascriptRuntime.channelFunctionsRegistered[flutterJs.getEngineInstanceId()];\n    if (channels != null && channels.containsKey('file.readAsString')) {\n      return;\n    }\n    var result = flutterJs.evaluate(code);\n    if (result.isError) {\n      logger.e('registerFile error: ${result.stringResult}');\n    }\n\n    flutterJs.onMessage('getApplicationSupportDirectory', (args) {\n      return getApplicationSupportDirectory().then((dir) => dir.path);\n    });\n\n    flutterJs.onMessage('file.readAsString', (path) {\n      return File(path).readAsString();\n    });\n\n    flutterJs.onMessage('file.readAsStringSync', (path) {\n      var readAsStringSync = File(path).readAsStringSync();\n      return readAsStringSync;\n    });\n\n    flutterJs.onMessage('file.readAsBytes', (path) {\n      return File(path).readAsBytes();\n    });\n\n    flutterJs.onMessage('file.readAsBytesSync', (path) {\n      return File(path).readAsBytesSync();\n    });\n\n    flutterJs.onMessage('file.writeAsString', (args) async {\n      var path = args['path'];\n      var content = args['content'];\n      var append = args['append'] ?? false;\n      await File(path).writeAsString(content, mode: append ? FileMode.append : FileMode.write);\n    });\n\n    flutterJs.onMessage('file.writeAsStringSync', (args) {\n      var path = args['path'];\n      var content = args['content'];\n      var append = args['append'] ?? false;\n\n      File(path).writeAsStringSync(content, mode: append ? FileMode.append : FileMode.write);\n    });\n\n    flutterJs.onMessage('file.writeAsBytes', (args) async {\n      var path = args['path'];\n      var bytes = List<int>.from(args['bytes']);\n      var append = args['append'] ?? false;\n\n      await File(path).writeAsBytes(bytes, mode: append ? FileMode.append : FileMode.write);\n    });\n\n    flutterJs.onMessage('file.writeAsBytesSync', (args) {\n      var path = args['path'];\n      var bytes = List<int>.from(args['bytes']);\n      var append = args['append'] ?? false;\n\n      File(path).writeAsBytesSync(bytes, mode: append ? FileMode.append : FileMode.write);\n    });\n\n    flutterJs.onMessage('file.length', (path) {\n      return File(path).length();\n    });\n\n    flutterJs.onMessage('file.lengthSync', (path) {\n      return File(path).lengthSync();\n    });\n\n    // flutterJs.onMessage('file.delete', (path) {\n    //   return File(path).delete();\n    // });\n    //\n    // flutterJs.onMessage('file.deleteSync', (path) {\n    //   return File(path).deleteSync();\n    // });\n\n    flutterJs.onMessage('file.exists', (path) {\n      return File(path).exists();\n    });\n\n    flutterJs.onMessage('file.existsSync', (path) {\n      return File(path).existsSync();\n    });\n\n    flutterJs.onMessage('file.create', (args) {\n      //bool recursive = false, bool exclusive = false\n      var path = args['path'];\n      var recursive = args['recursive'] ?? false;\n      var exclusive = args['exclusive'] ?? false;\n      File(path).create(recursive: recursive, exclusive: exclusive);\n    });\n\n    flutterJs.onMessage('file.createSync', (args) {\n      var path = args['path'];\n      var recursive = args['recursive'] ?? false;\n      var exclusive = args['exclusive'] ?? false;\n      File(path).createSync(recursive: recursive, exclusive: exclusive);\n    });\n\n    flutterJs.onMessage('file.rename', (args) async {\n      var path = args['path'];\n      var newPath = args['newPath'];\n      await File(path).rename(newPath);\n    });\n  }\n}\n"
  },
  {
    "path": "lib/network/components/js/md5.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter_js/flutter_js.dart';\nimport 'package:proxypin/network/util/lang.dart';\n\n/// JsMd5\n/// @author Hongen Wang\nclass Md5Bridge {\n  static const String _md5 = '''\n    function md5(input) {\n      return sendMessage('md5', JSON.stringify(input));\n    }\n  ''';\n\n  ///注册js md5\n  static void registerMd5(JavascriptRuntime flutterJs) {\n    var channels = JavascriptRuntime.channelFunctionsRegistered[flutterJs.getEngineInstanceId()];\n    if (channels != null && channels.containsKey('md5')) {\n      return;\n    }\n\n    flutterJs.evaluate(_md5);\n\n    flutterJs.onMessage('md5', (args) {\n      List<int> input;\n      //判断是否是二进制\n      if (Lists.getElementType(args) == int) {\n        input = Lists.convertList<int>(args);\n      } else {\n        input = utf8.encode(args.toString());\n      }\n\n      return md5.convert(input).toString();\n    });\n  }\n}\n"
  },
  {
    "path": "lib/network/components/js/script_engine.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter_js/flutter_js.dart';\nimport 'package:proxypin/network/components/js/xhr.dart';\n\nimport '../../http/http.dart';\nimport '../../http/http.dart' as http;\nimport '../../http/http_headers.dart';\nimport '../../util/lang.dart';\nimport '../../util/logger.dart';\nimport '../../util/uri.dart';\nimport 'file.dart';\nimport 'md5.dart';\n\nclass JavaScriptEngine {\n  static Future<JavascriptRuntime> getJavaScript({Function(dynamic args)? consoleLog}) async {\n    final JavascriptRuntime flutterJs = getJavascriptRuntime(xhr: false);\n\n    // register channel callback\n    if (consoleLog != null) {\n      final channelCallbacks = JavascriptRuntime.channelFunctionsRegistered[flutterJs.getEngineInstanceId()];\n      channelCallbacks![\"ConsoleLog\"] = consoleLog;\n    }\n    Md5Bridge.registerMd5(flutterJs);\n    FileBridge.registerFile(flutterJs);\n\n    flutterJs.enableFetch2();\n    return flutterJs;\n  }\n\n  /// js结果转换\n  static Future<dynamic> jsResultResolve(JavascriptRuntime flutterJs, JsEvalResult jsResult) async {\n    try {\n      if (jsResult.isPromise || jsResult.rawResult is Future) {\n        jsResult = await flutterJs.handlePromise(jsResult);\n      }\n\n      if (jsResult.isPromise || jsResult.rawResult is Future) {\n        jsResult = await flutterJs.handlePromise(jsResult);\n      }\n    } catch (e) {\n      throw SignalException(jsResult.stringResult);\n    }\n\n    var result = jsResult.rawResult;\n    if (Platform.isMacOS || Platform.isIOS) {\n      result = flutterJs.convertValue(jsResult);\n    }\n    if (result is String) {\n      result = jsonDecode(result);\n    }\n    if (jsResult.isError) {\n      logger.e('jsResultResolve error: ${jsResult.stringResult}');\n      throw SignalException(jsResult.stringResult);\n    }\n    return result;\n  }\n\n  //转换js request\n  static Future<Map<String, dynamic>> convertJsRequest(HttpRequest request) async {\n    var requestUri = request.requestUri;\n    return {\n      'host': requestUri?.host,\n      'url': request.requestUrl,\n      'path': requestUri?.path,\n      'queries': requestUri?.queryParameters,\n      'headers': request.headers.toMap(),\n      'method': request.method.name,\n      'body': await request.decodeBodyString(),\n      'rawBody': request.body\n    };\n  }\n\n  //转换js response\n  static Future<Map<String, dynamic>> convertJsResponse(HttpResponse response) async {\n    dynamic body = await response.decodeBodyString();\n    if (response.contentType.isBinary) {\n      body = response.body;\n    }\n\n    return {\n      'headers': response.headers.toMap(),\n      'statusCode': response.status.code,\n      'body': body,\n      'rawBody': response.body\n    };\n  }\n\n  //http request\n  static HttpRequest convertHttpRequest(HttpRequest request, Map<dynamic, dynamic> map) {\n    request.headers.clear();\n    request.method = http.HttpMethod.values.firstWhere((element) => element.name == map['method']);\n    String query = UriUtils.mapToQuery(map['queries']);\n\n    var requestUri = request.requestUri!.replace(path: map['path'], query: query);\n    if (requestUri.isScheme('https')) {\n      var query = requestUri.query;\n      request.uri = requestUri.path + (query.isNotEmpty ? '?${requestUri.query}' : '');\n    } else {\n      request.uri = requestUri.toString();\n    }\n\n    map['headers'].forEach((key, value) {\n      if (value is List) {\n        request.headers.addValues(key, value.map((e) => e.toString()).toList());\n        return;\n      }\n      request.headers.set(key, value);\n    });\n\n    request.headers.remove(HttpHeaders.CONTENT_ENCODING);\n\n    //判断是否是二进制\n    if (Lists.getElementType(map['body']) == int) {\n      request.body = Lists.convertList<int>(map['body']);\n      return request;\n    }\n\n    request.body = map['body']?.toString().codeUnits;\n\n    if (request.body != null && (request.charset == 'utf-8' || request.charset == 'utf8')) {\n      request.body = utf8.encode(map['body'].toString());\n    }\n    return request;\n  }\n\n  //http response\n  static HttpResponse convertHttpResponse(HttpResponse response, Map<dynamic, dynamic> map) {\n    response.headers.clear();\n    response.status = HttpStatus.valueOf(map['statusCode']);\n    map['headers'].forEach((key, value) {\n      if (value is List) {\n        response.headers.addValues(key, value.map((e) => e.toString()).toList());\n        return;\n      }\n\n      response.headers.set(key, value);\n    });\n\n    response.headers.remove(HttpHeaders.CONTENT_ENCODING);\n\n    //判断是否是二进制\n    if (Lists.getElementType(map['body']) == int) {\n      response.body = Lists.convertList<int>(map['body']);\n      return response;\n    }\n\n    response.body = map['body']?.toString().codeUnits;\n    if (response.body != null && (response.charset == 'utf-8' || response.charset == 'utf8')) {\n      response.body = utf8.encode(map['body'].toString());\n    }\n\n    return response;\n  }\n}\n"
  },
  {
    "path": "lib/network/components/js/xhr.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter_js/javascript_runtime.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:http/io_client.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/util/file_read.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/utils/platform.dart';\n\n/*\n * Based on bits and pieces from different OSS sources\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\n// ignore: non_constant_identifier_names\nvar _XHR_DEBUG = false;\n\nsetXhrDebug(bool value) => _XHR_DEBUG = value;\n\nconst HTTP_GET = \"get\";\nconst HTTP_POST = \"post\";\nconst HTTP_PATCH = \"patch\";\nconst HTTP_DELETE = \"delete\";\nconst HTTP_PUT = \"put\";\nconst HTTP_HEAD = \"head\";\n\nenum HttpMethod { put, get, post, delete, patch, head }\n\nString _debugSendNativeCallback() {\n  if (_XHR_DEBUG) {\n    return \"\"\"console.log(\"XMLHttpRequest._send_native_callback\");\n      console.log(\"arguments\");\n      console.log(arguments);\n      console.log(responseInfo);\n      console.log(responseText);\n      console.log(error);\"\"\";\n  } else\n    return \"\";\n}\n\nfinal String xhrJsCode = \"\"\"\nfunction XMLHttpRequest() {\n  this._send_native = XMLHttpRequestExtension_send_native;\n  this._httpMethod = null;\n  this._url = null;\n  this._requestHeaders = [];\n  this._responseHeaders = [];\n  this.response = null;\n  this.responseText = null;\n  this.responseXML = null;\n  this.onreadystatechange = null;\n  this.onloadstart = null;\n  this.onprogress = null;\n  this.onabort = null;\n  this.onerror = null;\n  this.onload = null;\n  this.onloadend = null;\n  this.ontimeout = null;\n  this.readyState = 0;\n  this.status = 0;\n  this.statusText = \"\";\n  this.withCredentials = null;\n};\n// readystate enum\nXMLHttpRequest.UNSENT = 0;\nXMLHttpRequest.OPENED = 1;\nXMLHttpRequest.HEADERS = 2;\nXMLHttpRequest.LOADING = 3;\nXMLHttpRequest.DONE = 4;\nXMLHttpRequest.prototype.constructor = XMLHttpRequest;\nXMLHttpRequest.prototype.open = function(httpMethod, url) {\n  this._httpMethod = httpMethod;\n  this._url = url;\n  this.readyState = XMLHttpRequest.OPENED;\n  if (typeof this.onreadystatechange === \"function\") {\n    //console.log(\"Calling onreadystatechange(OPENED)...\");\n    this.onreadystatechange();\n  }\n};\nXMLHttpRequest.prototype.send = function(data) {\n  this.readyState = XMLHttpRequest.LOADING;\n  if (typeof this.onreadystatechange === \"function\") {\n    //console.log(\"Calling onreadystatechange(LOADING)...\");\n    this.onreadystatechange();\n  }\n  if (typeof this.onloadstart === \"function\") {\n    //console.log(\"Calling onloadstart()...\");\n    this.onloadstart();\n  }\n  var that = this;\n  this._send_native(this._httpMethod, this._url, this._requestHeaders, data || null, function(responseInfo, responseText, error) {\n    that._send_native_callback(responseInfo, responseText, error);\n  }, this);\n};\nXMLHttpRequest.prototype.abort = function() {\n  this.readyState = XMLHttpRequest.UNSENT;\n  // Note: this.onreadystatechange() is not supposed to be called according to the XHR specs\n}\n// responseInfo: {statusCode, statusText, responseHeaders}\nXMLHttpRequest.prototype._send_native_callback = function(responseInfo, responseText, error) {\n  ${_debugSendNativeCallback()}\n  if (this.readyState === XMLHttpRequest.UNSENT) {\n    console.log(\"XHR native callback ignored because the request has been aborted\");\n    if (typeof this.onabort === \"function\") {\n      //console.log(\"Calling onabort()...\");\n      this.onabort();\n    }\n    return;\n  }\n  if (this.readyState != XMLHttpRequest.LOADING) {\n    // Request was not expected\n    console.log(\"XHR native callback ignored because the current state is not LOADING\");\n    return;\n  }\n  // Response info\n  // TODO: responseXML?\n  this.responseURL = this._url;\n  this.status = responseInfo.statusCode;\n  this.statusText = responseInfo.statusText;\n  this.responseBody = responseInfo.body;\n  this._responseHeaders = responseInfo.responseHeaders || [];\n  this.readyState = XMLHttpRequest.DONE;\n  // Response\n  this.response = null;\n  this.responseText = null;\n  this.responseXML = null;\n  if (error) {\n    this.responseText = error;\n  } else {\n    this.responseText = responseText;\n    this.response = {\n      body: responseInfo.body,\n    }\n    // console.log('RESPONSE TEXT: ' + responseText);\n  }\n  this.readyState = XMLHttpRequest.DONE;\n  if (typeof this.onreadystatechange === \"function\") {\n    //console.log(\"Calling onreadystatechange(DONE)...\");\n    this.onreadystatechange();\n  }\n  if (error === \"timeout\") {\n    // Timeout\n    console.warn(\"Got XHR timeout\");\n    if (typeof this.ontimeout === \"function\") {\n      //console.log(\"Calling ontimeout()...\");\n      this.ontimeout();\n    }\n  } else if (error) {\n    // Error\n    console.warn(\"Got XHR error:\", error);\n    if (typeof this.onerror === \"function\") {\n      //console.log(\"Calling onerror()...\");\n      this.onerror();\n    }\n  } else {\n    // Success\n    //console.log(\"XHR success: response =\", this.response);\n    if (typeof this.onload === \"function\") {\n      //console.log(\"Calling onload()...\");\n      this.onload();\n    }\n  }\n  if (typeof this.onloadend === \"function\") {\n    //console.log(\"Calling onloadend()...\");\n    this.onloadend();\n  }\n};\nXMLHttpRequest.prototype.setRequestHeader = function(header, value) {\n  this._requestHeaders.push([header, value]);\n};\nXMLHttpRequest.prototype.getAllResponseHeaders = function() {\n  var ret = \"\";\n  for (var i = 0; i < this._responseHeaders.length; i++) {\n    var keyValue = this._responseHeaders[i];\n    ret += keyValue[0] + \": \" + keyValue[1] + \"\\\\r\\\\n\";\n  }\n  return ret;\n};\nXMLHttpRequest.prototype.getResponseHeader = function(name) {\n  var ret = \"\";\n  for (var i = 0; i < this._responseHeaders.length; i++) {\n    var keyValue = this._responseHeaders[i];\n    if (keyValue[0] !== name) continue;\n    if (ret === \"\") ret += \", \";\n    ret += keyValue[1];\n  }\n  return ret;\n};\n// XMLHttpRequest.prototype.overrideMimeType = function() {\n//   // TODO\n// };\nthis.XMLHttpRequest = XMLHttpRequest;\"\"\";\n\nRegExp regexpHeader = RegExp(\"^([\\\\w-])+:(?!\\\\s*\\$).+\\$\");\n\nclass XhrPendingCall {\n  int? idRequest;\n  String? method;\n  String? url;\n  Map<String, String> headers;\n  String? body;\n\n  XhrPendingCall({\n    required this.idRequest,\n    required this.method,\n    required this.url,\n    required this.headers,\n    required this.body,\n  });\n}\n\nconst XHR_PENDING_CALLS_KEY = \"xhrPendingCalls\";\n\nhttp.Client? httpClient;\n\nxhrSetHttpClient(http.Client client) {\n  httpClient = client;\n}\n\nextension JavascriptRuntimeXhrExtension on JavascriptRuntime {\n  List<dynamic>? getPendingXhrCalls() {\n    return dartContext[XHR_PENDING_CALLS_KEY];\n  }\n\n  bool hasPendingXhrCalls() => getPendingXhrCalls()!.isNotEmpty;\n\n  void clearXhrPendingCalls() {\n    dartContext[XHR_PENDING_CALLS_KEY] = [];\n  }\n\n  Future<void> enableFetch2({bool enabledProxy = false}) async {\n    enableXhr2(enabledProxy: enabledProxy);\n    final fetchPolyfill = await FileRead.readAsString('assets/js/fetch.js');\n    final evalFetchResult = evaluate(fetchPolyfill);\n    logger.d('Eval Fetch Result: $evalFetchResult');\n  }\n\n  Future<http.Client> createClient(bool enabledProxy) async {\n    if (!enabledProxy) {\n      return http.Client();\n    }\n\n    // ProxyServer.current.isRunning\n    var httpClient = HttpClient();\n    String proxy;\n    if (Platforms.isDesktop()) {\n      Map? proxyResult = await DesktopMultiWindow.invokeMethod(0, 'getProxyInfo');\n      if (proxyResult == null) {\n        return http.Client();\n      }\n      proxy = \"${proxyResult['host']}:${proxyResult['port']}\";\n    } else {\n      if (ProxyServer.current?.isRunning == true) {\n        proxy = \"127.0.0.1:${ProxyServer.current!.port}\";\n      } else {\n        return http.Client();\n      }\n    }\n\n    httpClient.findProxy = (uri) {\n      return \"PROXY $proxy\";\n    };\n\n    httpClient.badCertificateCallback = (X509Certificate cert, String host, int port) => true;\n\n    // 创建一个 IOClient 实例，将 HttpClient 传入\n    return IOClient(httpClient);\n  }\n\n  void enableXhr2({bool enabledProxy = false}) async {\n    httpClient = httpClient ?? await createClient(enabledProxy);\n    dartContext[XHR_PENDING_CALLS_KEY] = [];\n\n    Timer.periodic(Duration(milliseconds: 40), (timer) {\n      // exits if there is no pending call to remote\n      if (!hasPendingXhrCalls()) return;\n\n      // collect the pending calls into a local variable making copies\n      List<dynamic> pendingCalls = List<dynamic>.from(getPendingXhrCalls()!);\n      // clear the global pending calls list\n      clearXhrPendingCalls();\n\n      // for each pending call, calls the remote http service\n      pendingCalls.forEach((element) async {\n        XhrPendingCall pendingCall = element as XhrPendingCall;\n        HttpMethod eMethod = HttpMethod.values\n            .firstWhere((e) => e.toString().toLowerCase() == (\"HttpMethod.${pendingCall.method}\".toLowerCase()));\n        late http.Response response;\n        switch (eMethod) {\n          case HttpMethod.head:\n            response = await httpClient!.head(\n              Uri.parse(pendingCall.url!),\n              headers: pendingCall.headers,\n            );\n            break;\n          case HttpMethod.get:\n            response = await httpClient!.get(\n              Uri.parse(pendingCall.url!),\n              headers: pendingCall.headers,\n            );\n            break;\n          case HttpMethod.post:\n            response = await httpClient!.post(\n              Uri.parse(pendingCall.url!),\n              body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body),\n              headers: pendingCall.headers,\n            );\n            break;\n          case HttpMethod.put:\n            response = await httpClient!.put(\n              Uri.parse(pendingCall.url!),\n              body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body),\n              headers: pendingCall.headers,\n            );\n            break;\n          case HttpMethod.patch:\n            response = await httpClient!.patch(\n              Uri.parse(pendingCall.url!),\n              body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body),\n              headers: pendingCall.headers,\n            );\n            break;\n          case HttpMethod.delete:\n            response = await httpClient!.delete(\n              Uri.parse(pendingCall.url!),\n              headers: pendingCall.headers,\n            );\n            break;\n        }\n        // assuming request was successfully executed\n        String? responseText;\n        List<int>? body;\n        try {\n          responseText = utf8.decode(response.bodyBytes);\n          responseText = jsonEncode(json.decode(responseText));\n        } on Exception {\n          // responseText = response.body;\n          body = response.bodyBytes;\n        }\n\n        final xhrResult = XmlHttpRequestResponse(\n          responseText: responseText,\n          responseInfo: XhtmlHttpResponseInfo(statusCode: 200, statusText: \"OK\", body: body),\n        );\n\n        response.headers.forEach((key, value) {\n          xhrResult.responseInfo?.addResponseHeaders(key, value);\n        });\n\n        final responseInfo = jsonEncode(xhrResult.responseInfo);\n        final safeResponseText = responseText != null ? jsonEncode(responseText) : null;\n        final error = xhrResult.error;\n        // logger.d('XHR response for url: ${pendingCall.url}, status: ${xhrResult.responseInfo?.statusCode}');\n\n        // send back to the javascript environment the\n        // response for the http pending callback\n        var jsResult = evaluate(\n          \"globalThis.xhrRequests[${pendingCall.idRequest}].callback($responseInfo, $safeResponseText, $error);\",\n        );\n        if (jsResult.isError) {\n          logger.e('jsResult error url:${pendingCall.url}, ${jsResult.stringResult}');\n        }\n      });\n    });\n\n    this.evaluate(\"\"\"\n    var xhrRequests = {};\n    var idRequest = -1;\n    function XMLHttpRequestExtension_send_native() {\n      idRequest += 1;\n      var cb = arguments[4];\n      var context = arguments[5];\n      xhrRequests[idRequest] = {\n        callback: function(responseInfo, responseText, error) {\n          cb(responseInfo, responseText, error);\n        }\n      };\n      var args = [];\n      args[0] = arguments[0];\n      args[1] = arguments[1];\n      args[2] = arguments[2];\n      args[3] = arguments[3];\n      args[4] = idRequest;\n      sendMessage('SendNative', JSON.stringify(args));\n    }\n    \"\"\");\n\n    final evalXhrResult = this.evaluate(xhrJsCode);\n\n    if (_XHR_DEBUG) print('RESULT evalXhrResult: $evalXhrResult');\n\n    this.onMessage('SendNative', (arguments) {\n      try {\n        String? method = arguments[0];\n        String? url = arguments[1];\n        dynamic headersList = arguments[2];\n        String? body = arguments[3];\n        int? idRequest = arguments[4];\n\n        Map<String, String> headers = {};\n        headersList.forEach((header) {\n          // final headerMatch = regexpHeader.allMatches(value).first;\n          // String? headerName = headerMatch.group(0);\n          // String? headerValue = headerMatch.group(1);\n          // if (headerName != null) {\n          //   headers[headerName] = headerValue ?? '';\n          // }\n          String headerKey = header[0];\n          headers[headerKey] = header[1];\n        });\n        (dartContext[XHR_PENDING_CALLS_KEY] as List<dynamic>).add(\n          XhrPendingCall(\n            idRequest: idRequest,\n            method: method,\n            url: url,\n            headers: headers,\n            body: body,\n          ),\n        );\n      } on Error catch (e) {\n        if (_XHR_DEBUG) print('ERROR calling sendNative on Dart: >>>> $e');\n      } on Exception catch (e) {\n        if (_XHR_DEBUG) print('Exception calling sendNative on Dart: >>>> $e');\n      }\n    });\n  }\n}\n\nclass XhtmlHttpResponseInfo {\n  final int? statusCode;\n  final String? statusText;\n  final List<int>? body;\n  final List<List<String>> responseHeaders = [];\n\n  XhtmlHttpResponseInfo({\n    this.body,\n    this.statusCode,\n    this.statusText,\n  });\n\n  void addResponseHeaders(String name, String value) {\n    responseHeaders.add([name, value]);\n  }\n\n  Map<String, Object?> toJson() {\n    return {\n      \"statusCode\": statusCode,\n      \"statusText\": statusText,\n      \"body\": body,\n      \"responseHeaders\": responseHeaders\n    };\n  }\n}\n\nclass XmlHttpRequestResponse {\n  final String? responseText;\n  final String? error; // should be timeout in case of timeout\n  final XhtmlHttpResponseInfo? responseInfo;\n\n  XmlHttpRequestResponse({this.responseText, this.responseInfo, this.error});\n\n  Map<String, Object?> toJson() {\n    return {'responseText': responseText, 'responseInfo': responseInfo!.toJson(), 'error': error};\n  }\n}\n"
  },
  {
    "path": "lib/network/components/manager/hosts_manager.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:proxypin/network/util/random.dart';\n\n/// Hosts manager\n/// @author wanghongen\nclass HostsManager {\n  static String separator = Platform.pathSeparator;\n\n  static HostsManager? _instance;\n  bool enabled = true;\n  final List<HostsItem> list = [];\n\n  final Map<String, List<HostsItem>> _folderMap = {};\n\n  HostsManager._();\n\n  /// Singleton\n  static Future<HostsManager> get instance async {\n    if (_instance == null) {\n      _instance = HostsManager._();\n      await _instance?.load();\n    }\n    return _instance!;\n  }\n\n  static File? _configFile;\n\n  static Future<String> homePath() async {\n    if (Platform.isMacOS) {\n      return await DesktopMultiWindow.invokeMethod(0, \"getApplicationSupportDirectory\");\n    }\n    return await getApplicationSupportDirectory().then((it) => it.path);\n  }\n\n  static Future<File> get configFile async {\n    if (_configFile != null) return _configFile!;\n\n    final path = await homePath();\n    var file = File('$path${separator}hosts.json');\n    if (!await file.exists()) {\n      await file.create();\n    }\n    _configFile = file;\n    return file;\n  }\n\n  /// Load\n  Future<void> load() async {\n    var json = await (await configFile).readAsString();\n    if (json.isEmpty) return;\n\n    var config = jsonDecode(json);\n    enabled = config['enabled'] == true;\n    list.clear();\n    config['list']?.forEach((element) {\n      var hostsItem = HostsItem.fromJson(element);\n\n      if (hostsItem.parent != null) {\n        var children = _folderMap[hostsItem.parent!] ??= [];\n        children.add(hostsItem);\n        return;\n      }\n\n      if (hostsItem.isFolder) {\n        _folderMap[hostsItem.id] ??= [];\n      }\n      list.add(hostsItem);\n    });\n  }\n\n  /// Save\n  Future<void> flushConfig() async {\n    var config = List.from(list);\n    for (var values in _folderMap.values) {\n      config.addAll(values);\n    }\n\n    var json = jsonEncode({\n      'enabled': enabled,\n      'list': config.map((e) => e.toJson()).toList(),\n    });\n    (await configFile).writeAsString(json);\n  }\n\n  List<HostsItem> getFolderList(String parent) {\n    return _folderMap[parent] ?? [];\n  }\n\n  Future<void> addHosts(HostsItem item) async {\n    if (item.parent == null) {\n      list.add(item);\n    } else {\n      var children = _folderMap[item.parent!] ??= [];\n      children.add(item);\n    }\n  }\n\n  Future<HostsItem?> getHosts(String host) async {\n    if (!enabled) return null;\n\n    for (var item in list) {\n      if (!item.enabled) continue;\n\n      if (item.isFolder) {\n        var list = _folderMap[item.id];\n        if (list == null) continue;\n        for (var it in list) {\n          if (it.enabled && it.match(host)) {\n            return it;\n          }\n        }\n        continue;\n      }\n\n      if (item.match(host)) {\n        return item;\n      }\n    }\n\n    return null;\n  }\n\n  removeHosts(Iterable<HostsItem> items) async {\n    if (items.isEmpty) return;\n    for (var item in items) {\n      if (item.parent == null) {\n        list.remove(item);\n        if (item.isFolder) {\n          _folderMap.remove(item.id);\n        }\n      } else {\n        var children = _folderMap[item.parent!] ??= [];\n        children.remove(item);\n      }\n    }\n    flushConfig();\n  }\n}\n\nclass HostsItem {\n  bool enabled = true;\n  bool isFolder = false;\n  final String id;\n  String? parent;\n  String host;\n  String? toAddress;\n  RegExp? _hostReg;\n\n  HostsItem({String? id, required this.host, this.toAddress, required this.enabled, this.isFolder = false, this.parent})\n      : id = id ?? generateId();\n\n  static String generateId() {\n    return DateTime.now().millisecondsSinceEpoch.toRadixString(36) + RandomUtil.randomString(4);\n  }\n\n  //匹配url\n  bool match(String domain) {\n    if (host != _hostReg?.pattern) _hostReg = null;\n    _hostReg ??= RegExp(host.replaceAll(\"*\", \".*\"));\n    return _hostReg!.hasMatch(domain);\n  }\n\n  factory HostsItem.fromJson(Map<String, dynamic> json) {\n    return HostsItem(\n      id: json['id'],\n      host: json['host'],\n      toAddress: json['toAddress'],\n      enabled: json['enabled'],\n      parent: json['parent'],\n      isFolder: json['isFolder'] == true,\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'id': id,\n      'parent': parent,\n      'enabled': enabled,\n      'isFolder': isFolder,\n      'host': host,\n      'toAddress': toAddress,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/network/components/manager/report_server_manager.dart",
    "content": "import 'dart:convert';\n\nimport '../../../storage/path.dart';\nimport '../../util/logger.dart';\n\nclass ReportServerManager {\n  static ReportServerManager? _instance;\n\n  List<ReportServer> _list = [];\n\n  ///单例\n  static Future<ReportServerManager> get instance async {\n    if (_instance == null) {\n      _instance = ReportServerManager._internal();\n      await _instance!.loadConfig();\n    }\n    return _instance!;\n  }\n\n  // Private constructor\n  ReportServerManager._internal();\n\n  /// Get configured report servers\n  List<ReportServer> get servers => _list;\n\n  Future<ReportServer?> matchServer(String url) async {\n    final list = servers;\n    for (var server in list) {\n      if (server.match(url)) {\n        return server;\n      }\n    }\n    return null;\n  }\n\n  Future<void> add(ReportServer server) async {\n    _list.add(server);\n    await _flush();\n  }\n\n  Future<void> removeAt(int index) async {\n    final list = servers;\n    list.removeAt(index);\n    await _flush();\n  }\n\n  Future<void> update(int index, ReportServer server) async {\n    final list = servers;\n    server.updateUrlReg();\n    list[index] = server;\n    await _flush();\n  }\n\n  Future<void> toggleEnabled(int index, bool enabled) async {\n    final list = servers;\n    list[index] = list[index].copyWith(enabled: enabled);\n    await _flush();\n  }\n\n  Future<void> loadConfig() async {\n    var list = <ReportServer>[];\n    final file = await Paths.getPath(\"report_servers.json\");\n    if (await file.exists()) {\n      final content = await file.readAsString();\n      if (content.trim().isNotEmpty) {\n        try {\n          final decoded = jsonDecode(content) as List<dynamic>;\n          list = decoded.map((e) => ReportServer.fromJson(e as Map<String, dynamic>)).toList();\n        } catch (e, t) {\n          logger.e('上报服务器配置解析失败', error: e, stackTrace: t);\n        }\n      }\n    }\n\n    _list = list;\n  }\n\n  Future<void> _flush() async {\n    final file = await Paths.getPath(\"report_servers.json\");\n    final list = servers;\n    await file.writeAsString(jsonEncode(list.map((e) => e.toJson()).toList()));\n  }\n}\n\nclass ReportServer {\n  final String name;\n\n  final String matchUrl;\n\n  /// 服务器URL\n  final String serverUrl;\n\n  /// 是否启用\n  final bool enabled;\n\n  /// 压缩方式：none/gzip，默认 none\n  final String? compression;\n\n  RegExp _urlReg;\n\n  ReportServer({\n    required this.name,\n    required this.matchUrl,\n    required this.serverUrl,\n    this.enabled = true,\n    this.compression,\n  }) : _urlReg = RegExp(matchUrl.replaceAll(\"*\", \".*\").replaceFirst('?', '\\\\?'));\n\n  bool match(String url) {\n    if (enabled) {\n      return _urlReg.hasMatch(url);\n    }\n    return false;\n  }\n\n  void updateUrlReg() {\n    _urlReg = RegExp(matchUrl.replaceAll(\"*\", \".*\").replaceFirst('?', '\\\\?'));\n  }\n\n  ReportServer copyWith({\n    String? name,\n    String? serverUrl,\n    bool? enabled,\n    String? matchUrl,\n    String? matchType,\n    String? compression,\n    Map<String, String>? headers,\n  }) {\n    return ReportServer(\n      name: name ?? this.name,\n      matchUrl: matchUrl ?? this.matchUrl,\n      serverUrl: serverUrl ?? this.serverUrl,\n      enabled: enabled ?? this.enabled,\n      compression: compression ?? this.compression,\n    );\n  }\n\n  factory ReportServer.fromJson(Map<String, dynamic> json) {\n    return ReportServer(\n      name: json['name'] ?? '',\n      matchUrl: json['matchUrl'] ?? '',\n      serverUrl: json['serverUrl'] ?? '',\n      enabled: json['enabled'] ?? true,\n      compression: (json['compression'] ?? 'none') as String,\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'name': name,\n      'matchUrl': matchUrl,\n      'serverUrl': serverUrl,\n      'enabled': enabled,\n      'compression': compression,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/network/components/manager/request_block_manager.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:path_provider/path_provider.dart';\n\n/// 请求屏蔽\n/// @author wanghongen\n/// 2024/02/02\nclass RequestBlockManager {\n  static RequestBlockManager? _instance;\n  bool enabled = true;\n  List<RequestBlockItem> list = [];\n  final File _storageFile;\n\n  RequestBlockManager._(this._storageFile);\n\n  ///单例\n  static Future<RequestBlockManager> get instance async {\n    if (_instance == null) {\n      var file = await configFile();\n      _instance = RequestBlockManager._(file);\n      await _instance?._load();\n    }\n    return _instance!;\n  }\n\n  static Future<File> configFile() async {\n    var directory = await getApplicationSupportDirectory().then((it) => it.path);\n    var file = File('$directory${Platform.pathSeparator}request_block.json');\n    if (!await file.exists()) {\n      await file.create(recursive: true);\n    }\n    return file;\n  }\n\n  ///加载\n  Future<void> _load() async {\n    var json = await _storageFile.readAsString();\n    if (json.isEmpty) return;\n    var config = jsonDecode(json);\n    enabled = config['enabled'] == true;\n    list.clear();\n    config['list']?.forEach((element) {\n      list.add(RequestBlockItem.fromJson(element));\n    });\n  }\n\n  addBlockRequest(RequestBlockItem item) {\n    list.add(item);\n    flushConfig();\n  }\n\n  removeBlockRequest(int index) {\n    list.removeAt(index);\n    flushConfig();\n  }\n\n  /// 是否启用\n  bool enableBlockRequest(String url) {\n    if (!enabled) {\n      return false;\n    }\n    return list.any((element) => element.match(url, BlockType.blockRequest));\n  }\n\n  bool enableBlockResponse(String url) {\n    if (!enabled) {\n      return false;\n    }\n    return list.any((element) => element.match(url, BlockType.blockResponse));\n  }\n\n  ///刷新配置\n  Future<void> flushConfig() async {\n    _storageFile.writeAsString(jsonEncode({'enabled': enabled, 'list': list}));\n  }\n}\n\nenum BlockType {\n  blockRequest('屏蔽请求'),\n  blockResponse('屏蔽响应');\n\n  //名称\n  final String label;\n\n  const BlockType(this.label);\n  static BlockType nameOf(String name) {\n    return BlockType.values.firstWhere((element) => element.name == name);\n  }\n}\n\nclass RequestBlockItem {\n  bool enabled = true;\n  String url;\n  BlockType type;\n  RegExp? urlReg;\n\n  RequestBlockItem(this.enabled, this.url, this.type);\n\n  //匹配url\n  bool match(String url, BlockType blockType) {\n    urlReg ??= RegExp(this.url.replaceAll(\"*\", \".*\"));\n    return enabled && type == blockType && urlReg!.hasMatch(url);\n  }\n\n  factory RequestBlockItem.fromJson(Map<String, dynamic> json) {\n    return RequestBlockItem(json['enabled'], json['url'], BlockType.nameOf(json['type']));\n  }\n\n  Map<String, dynamic> toJson() {\n    return {'enabled': enabled, 'url': url, 'type': type.name};\n  }\n\n  @override\n  String toString() {\n    return toJson().toString();\n  }\n}\n"
  },
  {
    "path": "lib/network/components/manager/request_breakpoint_manager.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nclass RequestBreakpointRule {\n  bool enabled;\n  String? name;\n  String url;\n\n  bool interceptRequest;\n  bool interceptResponse;\n\n  // Optional HTTP method matching; null means match any method\n  HttpMethod? method;\n\n  RequestBreakpointRule({\n    this.enabled = true,\n    this.name,\n    required this.url,\n    this.interceptRequest = true,\n    this.interceptResponse = true,\n    this.method,\n  });\n\n  bool match(String url, {HttpMethod? method}) {\n    if (!enabled) return false;\n    if (this.method != null && method != null && this.method != method) return false;\n    return RegExp(this.url).hasMatch(url);\n  }\n\n  factory RequestBreakpointRule.fromJson(Map<dynamic, dynamic> map) {\n    HttpMethod? method;\n    try {\n      if (map['method'] != null) {\n        method = HttpMethod.valueOf(map['method']);\n      }\n    } catch (e) {\n      logger.e('Failed to parse HTTP method from request intercept rule', error: e);\n    }\n\n    return RequestBreakpointRule(\n      enabled: map['enabled'] ?? true,\n      name: map['name'],\n      url: map['url'] ?? '',\n      interceptRequest: map['interceptRequest'] ?? true,\n      interceptResponse: map['interceptResponse'] ?? true,\n      method: method,\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'enabled': enabled,\n      'name': name,\n      'url': url,\n      'interceptRequest': interceptRequest,\n      'interceptResponse': interceptResponse,\n      'method': method?.name,\n    };\n  }\n}\n\nclass RequestBreakpointManager {\n  static RequestBreakpointManager? _instance;\n\n  RequestBreakpointManager._();\n\n  static Future<RequestBreakpointManager> get instance async {\n    if (_instance == null) {\n      _instance = RequestBreakpointManager._();\n      await _instance!.load();\n    }\n    return _instance!;\n  }\n\n  bool enabled = true;\n  List<RequestBreakpointRule> list = [];\n\n  static Future<String> homePath() async {\n    if (Platform.isMacOS) {\n      return await DesktopMultiWindow.invokeMethod(0, \"getApplicationSupportDirectory\");\n    }\n    return await getApplicationSupportDirectory().then((it) => it.path);\n  }\n\n  Future<void> load() async {\n    try {\n      var home = await homePath();\n      var file = File('$home${Platform.pathSeparator}request_breakpoint.json');\n      if (await file.exists()) {\n        var json = jsonDecode(await file.readAsString());\n        enabled = json['enabled'] ?? false;\n        list = (json['list'] as List? ?? []).map((e) => RequestBreakpointRule.fromJson(e)).toList();\n      }\n    } catch (e) {\n      logger.e('Failed to load request breakpoint config', error: e);\n    }\n  }\n\n  Future<void> save() async {\n    try {\n      var home = await homePath();\n      var file = File('$home${Platform.pathSeparator}request_breakpoint.json');\n      if (!await file.exists()) {\n        await file.create(recursive: true);\n      }\n      var json = {\n        'enabled': enabled,\n        'list': list.map((e) => e.toJson()).toList(),\n      };\n      await file.writeAsString(jsonEncode(json));\n    } catch (e) {\n      logger.e('Failed to save request breakpoint config', error: e);\n    }\n  }\n\n  void add(RequestBreakpointRule rule) {\n    list.add(rule);\n    save();\n  }\n\n  void remove(RequestBreakpointRule rule) {\n    list.remove(rule);\n    save();\n  }\n}\n"
  },
  {
    "path": "lib/network/components/manager/request_crypto_manager.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/file_read.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nclass RequestCryptoManager {\n  static String separator = Platform.pathSeparator;\n\n  static RequestCryptoManager? _instance;\n\n  RequestCryptoManager._();\n\n  static Future<RequestCryptoManager> get instance async {\n    if (_instance == null) {\n      final config = await _loadRequestCryptoConfig();\n      _instance = RequestCryptoManager._();\n      await _instance!._reload(config);\n    }\n    return _instance!;\n  }\n\n  bool enabled = true;\n  List<CryptoRule> rules = [];\n\n  Future<void> _reload(Map<String, dynamic>? map) async {\n    if (map == null) {\n      return;\n    }\n\n    enabled = map['enabled'] == true;\n    final list = map['rules'] as List<dynamic>? ?? const [];\n    rules = [];\n    for (final element in list) {\n      try {\n        rules.add(CryptoRule.fromJson(Map<String, dynamic>.from(element)));\n      } catch (e) {\n        logger.e('加载请求加解密配置失败 $element', error: e);\n      }\n    }\n  }\n\n  Future<void> reloadConfig() async {\n    final config = await _loadRequestCryptoConfig();\n    await _reload(config);\n  }\n\n  static Future<Map<String, dynamic>?> _loadRequestCryptoConfig() async {\n    final home = await FileRead.homeDir();\n    final file = File('${home.path}${Platform.pathSeparator}request_crypto.json');\n    if (!await file.exists()) {\n      return null;\n    }\n    try {\n      final json = jsonDecode(await file.readAsString()) as Map<String, dynamic>;\n      logger.i('加载请求加解密配置文件 [$file]');\n      return json;\n    } catch (e, stack) {\n      logger.e('解析请求加解密配置失败', error: e, stackTrace: stack);\n      return null;\n    }\n  }\n\n  Future<void> flushConfig() async {\n    final home = await FileRead.homeDir();\n    final file = File('${home.path}${Platform.pathSeparator}request_crypto.json');\n    if (!await file.exists()) {\n      await file.create(recursive: true);\n    }\n    final json = jsonEncode(toJson());\n    logger.i('刷新请求加解密配置文件 ${file.path}');\n    await file.writeAsString(json);\n  }\n\n  /// Get the first matching rule for the given URL and optional field name\n  CryptoRule? getMatchingRule(HttpMessage message) {\n    final url = message.requestUrl;\n    if (url == null) return null;\n    if (!enabled) return null;\n    for (final rule in rules) {\n      if (!rule.enabled || !rule.matches(url)) continue;\n      return rule;\n    }\n    return null;\n  }\n\n  /// Add a new crypto rule to the manager\n  Future<void> addRule(CryptoRule rule) async {\n    rules.add(rule);\n  }\n\n  /// Update an existing rule at [index]\n  Future<void> updateRule(int index, CryptoRule rule) async {\n    if (index < 0 || index >= rules.length) return;\n    rules[index] = rule;\n  }\n\n  /// Remove a single rule by index\n  Future<void> removeRule(int index) async {\n    if (index < 0 || index >= rules.length) return;\n    rules.removeAt(index);\n  }\n\n  /// Remove multiple rules. Indexes should be sorted or will be sorted descending.\n  Future<void> removeIndex(List<int> indexes) async {\n    indexes.sort((a, b) => b.compareTo(a));\n    for (final i in indexes) {\n      if (i >= 0 && i < rules.length) {\n        rules.removeAt(i);\n      }\n    }\n  }\n\n  Map<String, Object> toJson() => {\n        'enabled': enabled,\n        'rules': rules.map((e) => e.toJson()).toList(),\n      };\n}\n\nclass CryptoRule {\n  final String name;\n  final String urlPattern;\n  final String? field; // single field supported\n  bool enabled;\n  final CryptoKeyConfig config;\n\n  CryptoRule({\n    required this.name,\n    required this.urlPattern,\n    this.field,\n    required this.enabled,\n    required this.config,\n  });\n\n  bool matches(String url) {\n    try {\n      return RegExp(urlPattern).hasMatch(url);\n    } catch (_) {\n      return url.contains(urlPattern);\n    }\n  }\n\n  Map<String, dynamic> toJson() {\n    final map = <String, dynamic>{\n      'name': name,\n      'urlPattern': urlPattern,\n      'field': field,\n      'enabled': enabled,\n      'config': config.toJson(),\n    };\n    return map;\n  }\n\n  factory CryptoRule.fromJson(Map<String, dynamic> json) {\n    return CryptoRule(\n      name: json['name'] ?? '',\n      urlPattern: json['urlPattern'] ?? '',\n      field: json['field'],\n      enabled: json['enabled'] ?? true,\n      config: CryptoKeyConfig.fromJson(Map<String, dynamic>.from(json['config'] ?? {})),\n    );\n  }\n\n  CryptoRule copyWith({\n    String? name,\n    String? urlPattern,\n    String? field,\n    bool? enabled,\n    CryptoKeyConfig? config,\n  }) {\n    return CryptoRule(\n      name: name ?? this.name,\n      urlPattern: urlPattern ?? this.urlPattern,\n      field: field ?? this.field,\n      enabled: enabled ?? this.enabled,\n      config: config ?? this.config,\n    );\n  }\n\n  /// Legacy constructor used by UI to create a default empty AesRule\n  static CryptoRule newRule() {\n    return CryptoRule(\n      name: '',\n      urlPattern: '',\n      field: '',\n      enabled: true,\n      config: CryptoKeyConfig.defaults(),\n    );\n  }\n}\n\nclass CryptoKeyConfig {\n  final String key;\n  final String iv;\n  final String ivSource; // 'manual' or 'prefix'\n  final int ivPrefixLength;\n  final String mode;\n  final String padding;\n  final int keyLength;\n\n  const CryptoKeyConfig({\n    required this.key,\n    required this.iv,\n    required this.ivSource,\n    required this.ivPrefixLength,\n    required this.mode,\n    required this.padding,\n    required this.keyLength,\n  });\n\n  factory CryptoKeyConfig.defaults() {\n    return const CryptoKeyConfig(\n        key: '', iv: '', ivSource: 'manual', ivPrefixLength: 16, mode: 'ECB', padding: 'PKCS7', keyLength: 128);\n  }\n\n  bool get isReady {\n    if (key.trim().isEmpty) return false;\n    if (mode != 'CBC') return true;\n    // for CBC, either manual IV provided or prefix mode selected\n    if (ivSource == 'prefix') return true;\n    return iv.trim().isNotEmpty;\n  }\n\n  CryptoKeyConfig copyWith({\n    String? key,\n    String? iv,\n    String? ivSource,\n    int? ivPrefixLength,\n    String? mode,\n    String? padding,\n    int? keyLength,\n  }) {\n    return CryptoKeyConfig(\n      key: key ?? this.key,\n      iv: iv ?? this.iv,\n      ivSource: ivSource ?? this.ivSource,\n      ivPrefixLength: ivPrefixLength ?? this.ivPrefixLength,\n      mode: mode ?? this.mode,\n      padding: padding ?? this.padding,\n      keyLength: keyLength ?? this.keyLength,\n    );\n  }\n\n  Map<String, Object?> toJson() {\n    return {\n      'key': key,\n      'iv': iv,\n      'ivSource': ivSource,\n      'ivPrefixLength': ivPrefixLength,\n      'mode': mode,\n      'padding': padding,\n      'keyLength': keyLength,\n    };\n  }\n\n  factory CryptoKeyConfig.fromJson(Map<String, dynamic> json) {\n    return CryptoKeyConfig(\n      key: json['key'] ?? '',\n      iv: json['iv'] ?? '',\n      ivSource: json['ivSource'] ?? 'manual',\n      ivPrefixLength: json['ivPrefixLength'] ?? 16,\n      mode: json['mode'] ?? 'ECB',\n      padding: json['padding'] ?? 'PKCS7',\n      keyLength: json['keyLength'] ?? 128,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/network/components/manager/request_map_manager.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../util/logger.dart';\nimport '../../util/random.dart';\n\nclass RequestMapManager {\n  static RequestMapManager? _instance;\n\n  static String separator = Platform.pathSeparator;\n\n  RequestMapManager._internal();\n\n  final Map<RequestMapRule, RequestMapItem> _mapItemsCache = {};\n\n  bool enabled = true;\n\n  //存储所有的请求映射规则\n  List<RequestMapRule> rules = [];\n\n  ///单例\n  static Future<RequestMapManager> get instance async {\n    if (_instance == null) {\n      _instance = RequestMapManager._internal();\n      await _instance?.reloadConfig();\n    }\n    return _instance!;\n  }\n\n  //添加规则\n  Future<void> addRule(RequestMapRule rule, RequestMapItem item) async {\n    final path = await homePath();\n    String itemPath = \"${separator}request_map$separator${RandomUtil.randomString(16)}.json\";\n    var file = File(path + itemPath);\n    await file.create(recursive: true);\n    final itemJson = jsonEncode(item.toJson());\n    file.writeAsString(itemJson);\n\n    rule.itemPath = itemPath;\n    _mapItemsCache[rule] = item;\n    rules.add(rule);\n\n    await flushConfig();\n  }\n\n  //update rule\n  Future<void> updateRule(RequestMapRule rule, RequestMapItem item) async {\n    rule.updatePathReg();\n    if (rule.itemPath != null) {\n      final path = await homePath();\n      var file = File('$path${rule.itemPath}');\n      await file.writeAsString(jsonEncode(item.toJson()));\n    }\n    _mapItemsCache[rule] = item;\n    await flushConfig();\n  }\n\n  //删除规则\n  Future<void> deleteRule(int index) async {\n    var item = rules.removeAt(index);\n    final home = await homePath();\n    File(home + item.itemPath!).delete();\n  }\n\n  //根据url和类型查找匹配的规则\n  RequestMapRule? findMatch(String url) {\n    for (var rule in rules) {\n      if (rule.match(url)) {\n        return rule;\n      }\n    }\n    return null;\n  }\n\n  Future<RequestMapItem?> getMapItem(RequestMapRule rule) async {\n    if (_mapItemsCache.containsKey(rule)) {\n      return _mapItemsCache[rule];\n    }\n\n    if (rule.itemPath != null) {\n      final path = await homePath();\n      var file = File('$path$separator${rule.itemPath}');\n      if (await file.exists()) {\n        var content = await file.readAsString();\n        if (content.isNotEmpty) {\n          var item = RequestMapItem.fromJson(jsonDecode(content));\n          _mapItemsCache[rule] = item;\n          return item;\n        }\n      }\n    }\n    return null;\n  }\n\n  static String? _homePath;\n\n  static Future<String> homePath() async {\n    if (_homePath != null) {\n      return _homePath!;\n    }\n\n    if (Platform.isMacOS) {\n      _homePath = await DesktopMultiWindow.invokeMethod(0, \"getApplicationSupportDirectory\");\n    } else {\n      _homePath = await getApplicationSupportDirectory().then((it) => it.path);\n    }\n    return _homePath!;\n  }\n\n  static Future<File> get _path async {\n    final path = await homePath();\n    var file = File('$path${Platform.pathSeparator}request_map.json');\n    if (!await file.exists()) {\n      await file.create();\n    }\n    return file;\n  }\n\n  ///重新加载配置\n  Future<void> reloadConfig() async {\n    List<RequestMapRule> list = [];\n    var file = await _path;\n    logger.d(\"reload request map config from ${file.path}\");\n\n    if (await file.exists()) {\n      var content = await file.readAsString();\n      if (content.isEmpty) {\n        return;\n      }\n      var config = jsonDecode(content);\n      enabled = config['enabled'] == true;\n      for (var entry in config['list']) {\n        list.add(RequestMapRule.fromJson(entry));\n      }\n    }\n    rules = list;\n    _mapItemsCache.clear();\n  }\n\n  ///保存配置\n  Future<void> flushConfig() async {\n    var file = await _path;\n    if (!await file.exists()) {\n      await file.create(recursive: true);\n    }\n\n    var config = {\n      'enabled': enabled,\n      'list': rules.map((e) => e.toJson()).toList(),\n    };\n\n    await file.writeAsString(jsonEncode(config));\n  }\n}\n\nenum RequestMapType {\n  local(\"本地\"),\n  script(\"脚本\"),\n  ;\n\n  //名称\n  final String label;\n\n  const RequestMapType(this.label);\n\n  static RequestMapType fromName(String name) {\n    return values.firstWhere((element) => element.name == name || element.label == name);\n  }\n}\n\nclass RequestMapRule {\n  bool enabled;\n  RequestMapType type;\n\n  String? name;\n  String url;\n  RegExp _urlReg;\n  String? itemPath;\n\n  RequestMapRule({this.enabled = true, this.name, required this.url, required this.type, this.itemPath})\n      : _urlReg = RegExp(url.replaceAll(\"*\", \".*\").replaceFirst('?', '\\\\?'));\n\n  bool match(String url) {\n    if (enabled) {\n      return _urlReg.hasMatch(url);\n    }\n    return false;\n  }\n\n  /// 从json中创建\n  factory RequestMapRule.fromJson(Map<dynamic, dynamic> map) {\n    return RequestMapRule(\n        enabled: map['enabled'] == true,\n        name: map['name'],\n        url: map['url'],\n        type: RequestMapType.fromName(map['type']),\n        itemPath: map['itemPath']);\n  }\n\n  void updatePathReg() {\n    _urlReg = RegExp(url.replaceAll(\"*\", \".*\").replaceFirst('?', '\\\\?'));\n  }\n\n  Map<String, Object?> toJson() {\n    return {\n      'name': name,\n      'enabled': enabled,\n      'url': url,\n      'type': type.name,\n      'itemPath': itemPath,\n    };\n  }\n}\n\nclass RequestMapItem {\n  String? script;\n\n  int? statusCode;\n  Map<String, String>? headers;\n\n  //body\n  String? body;\n\n  String? bodyType;\n\n  String? bodyFile;\n\n  RequestMapItem({this.script, this.statusCode, this.headers, this.body, this.bodyType, this.bodyFile});\n\n  /// 从json中创建\n  factory RequestMapItem.fromJson(Map<dynamic, dynamic> map) {\n    return RequestMapItem(\n      script: map['script'],\n      statusCode: map['statusCode'],\n      headers: (map['headers'] as Map?)?.cast<String, String>(),\n      body: map['body'],\n      bodyType: map['bodyType'],\n      bodyFile: map['bodyFile'],\n    );\n  }\n\n  Map<String, Object?> toJson() {\n    return {\n      'script': script,\n      'statusCode': statusCode,\n      'headers': headers,\n      'body': body,\n      'bodyType': bodyType,\n      'bodyFile': bodyFile,\n    };\n  }\n}\n\nenum MapBodyType {\n  text(\"文本\"),\n  file(\"文件\");\n\n  final String label;\n\n  const MapBodyType(this.label);\n}\n"
  },
  {
    "path": "lib/network/components/manager/request_rewrite_manager.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/file_read.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/random.dart';\n\n/// @author wanghongen\n/// 2023/7/26\n/// 请求重写\nclass RequestRewriteManager {\n  static String separator = Platform.pathSeparator;\n\n  //重写规则\n  final Map<RequestRewriteRule, List<RewriteItem>> rewriteItemsCache = {};\n\n  //单例\n  static RequestRewriteManager? _instance;\n\n  RequestRewriteManager._();\n\n  static Future<RequestRewriteManager> get instance async {\n    if (_instance == null) {\n      var config = await _loadRequestRewriteConfig();\n      _instance = RequestRewriteManager._();\n      await _instance!.reload(config);\n    }\n    return _instance!;\n  }\n\n  bool enabled = true;\n  List<RequestRewriteRule> rules = [];\n\n  //重新加载配置\n  Future<void> reload(Map<String, dynamic>? map) async {\n    rewriteItemsCache.clear();\n    if (map == null) {\n      return;\n    }\n\n    enabled = map['enabled'] == true;\n    List list = map['rules'] ?? [];\n    rules.clear();\n    for (var element in list) {\n      try {\n        rules.add(RequestRewriteRule.formJson(element));\n      } catch (e) {\n        logger.e('加载请求重写配置失败 $element', error: e);\n      }\n    }\n  }\n\n  ///重新加载请求重写\n  Future<void> reloadRequestRewrite() async {\n    var config = await _loadRequestRewriteConfig();\n    reload(config);\n  }\n\n  ///同步配置\n  Future<void> syncConfig(Map<String, dynamic>? config) async {\n    if (config == null) {\n      return;\n    }\n\n    rewriteItemsCache.clear();\n    enabled = config['enabled'] == true;\n    List list = config['rules'] ?? [];\n    rules.clear();\n    for (var element in list) {\n      try {\n        var rule = RequestRewriteRule.formJson(element);\n        List list = element['items'] as List;\n        List<RewriteItem> items = list.map((e) => RewriteItem.fromJson(e)).toList();\n        await addRule(rule, items);\n      } catch (e) {\n        logger.e('加载请求重写配置失败 $element', error: e);\n      }\n    }\n    flushRequestRewriteConfig();\n  }\n\n  /// 加载请求重写配置文件\n  static Future<Map<String, dynamic>?> _loadRequestRewriteConfig() async {\n    var home = await FileRead.homeDir();\n    var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json');\n    var exits = await file.exists();\n    if (!exits) {\n      return null;\n    }\n\n    Map<String, dynamic> config = jsonDecode(await file.readAsString());\n    logger.i('加载请求重写配置文件 [$file]');\n    return config;\n  }\n\n  /// 保存请求重写配置文件\n  Future<void> flushRequestRewriteConfig() async {\n    var home = await FileRead.homeDir();\n    var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json');\n    bool exists = await file.exists();\n    if (!exists) {\n      await file.create(recursive: true);\n    }\n    var json = jsonEncode(toJson());\n    logger.i('刷新请求重写配置文件 ${file.path}');\n    await file.writeAsString(json);\n  }\n\n  ///添加规则\n  Future<void> addRule(RequestRewriteRule rule, List<RewriteItem> items) async {\n    final home = await FileRead.homeDir();\n\n    String rewritePath = \"${separator}rewrite$separator${RandomUtil.randomString(16)}.json\";\n    var file = File(home.path + rewritePath);\n    await file.create(recursive: true);\n    file.writeAsString(jsonEncode(items.map((e) => e.toJson()).toList()));\n    rule.rewritePath = rewritePath;\n\n    rules.add(rule);\n    rewriteItemsCache[rule] = items;\n  }\n\n  ///更新规则\n  Future<void> updateRule(int index, RequestRewriteRule rule, List<RewriteItem>? items) async {\n    rewriteItemsCache.remove(rules[index]);\n    final home = await FileRead.homeDir();\n    rule.updatePathReg();\n    rules[index] = rule;\n\n    if (items == null) {\n      return;\n    }\n    bool isExist = rule.rewritePath != null;\n    if (rule.rewritePath == null) {\n      String rewritePath = \"${separator}rewrite$separator${RandomUtil.randomString(16)}.json\";\n      rule.rewritePath = rewritePath;\n    }\n\n    File file = File(home.path + rule.rewritePath!);\n    if (!isExist) {\n      await file.create(recursive: true);\n    }\n\n    await file.writeAsString(jsonEncode(items.map((e) => e.toJson()).toList()));\n    rewriteItemsCache[rule] = items;\n  }\n\n  Future<void> removeIndex(List<int> indexes) async {\n    for (var i in indexes) {\n      var rule = rules.removeAt(i);\n      rewriteItemsCache.remove(rule); //删除缓存\n      if (rule.rewritePath != null) {\n        File home = await FileRead.homeDir();\n        try {\n          await File(home.path + rule.rewritePath!).delete();\n        } catch (e) {\n          logger.e('删除请求重写配置文件失败 ${home.path + rule.rewritePath!}', error: e);\n        }\n        rule.rewritePath = null;\n      }\n    }\n  }\n\n  RequestRewriteRule getRequestRewriteRule(HttpRequest request, RuleType type) {\n    var url = request.domainPath;\n    for (var rule in rules) {\n      if (rule.match(url, type: type, method: request.method) && rule.type == type) {\n        return rule;\n      }\n    }\n\n    return RequestRewriteRule(type: type, url: url);\n  }\n\n  RequestRewriteRule? getRewriteRule(String? url, List<RuleType> types) {\n    if (url == null || !enabled) {\n      return null;\n    }\n    for (var rule in rules) {\n      if (rule.match(url) && types.contains(rule.type)) {\n        return rule;\n      }\n    }\n    return null;\n  }\n\n  /// 获取重写规则\n  Future<List<RewriteItem>?> getRewriteItems(RequestRewriteRule rule) async {\n    if (rewriteItemsCache.containsKey(rule)) {\n      return rewriteItemsCache[rule]!;\n    }\n    if (rule.rewritePath == null) {\n      return null;\n    }\n\n    final home = await FileRead.homeDir();\n    List<RewriteItem> items = [];\n    try {\n      var json = await File(home.path + rule.rewritePath!).readAsString();\n      List? list = jsonDecode(json);\n      list?.forEach((element) => items.add(RewriteItem.fromJson(element)));\n      rewriteItemsCache[rule] = items;\n    } catch (e) {\n      logger.e('加载请求重写配置文件失败 ${home.path + rule.rewritePath!}', error: e);\n    }\n    return items;\n  }\n\n  Map<String, Object> toJson() {\n    return {\n      'enabled': enabled,\n      'rules': rules.map((e) => e.toJson()).toList(),\n    };\n  }\n\n  Future<Map<String, dynamic>> toFullJson() async {\n    var rulesJson = [];\n    for (var rule in rules) {\n      var json = rule.toJson();\n      json['items'] = await getRewriteItems(rule);\n      rulesJson.add(json);\n    }\n\n    return {\n      'enabled': enabled,\n      'rules': rulesJson,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/network/components/manager/rewrite_rule.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/utils/lang.dart';\n\n///重写规则\n///@author: wanghongen\nenum RuleType {\n  // body(\"重写消息体\"), //OLD VERSION\n\n  requestReplace(\"替换请求\"),\n  responseReplace(\"替换响应\"),\n  requestUpdate(\"修改请求\"),\n  responseUpdate(\"修改响应\"),\n  redirect(\"重定向\");\n\n  //名称\n  final String label;\n\n  const RuleType(this.label);\n\n  static RuleType fromName(String name) {\n    return values.firstWhere((element) => element.name == name || element.label == name);\n  }\n}\n\nclass RequestRewriteRule {\n  bool enabled;\n  RuleType type;\n\n  String? name;\n  String url;\n  RegExp _urlReg;\n  String? rewritePath;\n\n  // 可选的 HTTP 方法匹配；null 表示匹配任意方法\n  HttpMethod? method;\n\n  RequestRewriteRule({this.enabled = true, this.name, required this.url, required this.type, this.rewritePath, this.method})\n      : _urlReg = RegExp(url.replaceAll(\"*\", \".*\").replaceFirst('?', '\\\\?'));\n\n  bool match(String url, {RuleType? type, HttpMethod? method}) {\n    if (!enabled) return false;\n    if (type != null && this.type != type) return false;\n\n    // 如果调用方提供了 method，则当规则定义了 method 时进行比较；如果调用方未提供 method，则不按方法过滤（向后兼容）\n    if (method != null && this.method != null && this.method != method) return false;\n\n    return _urlReg.hasMatch(url);\n  }\n\n  bool matchUrl(String url, RuleType type) {\n    return this.type == type && _urlReg.hasMatch(url);\n  }\n\n  /// 从json中创建\n  factory RequestRewriteRule.formJson(Map<dynamic, dynamic> map) {\n    HttpMethod? method;\n    try {\n      if (map['method'] != null) {\n        method = HttpMethod.valueOf(map['method'].toString());\n      }\n    } catch (e) {\n      // ignore invalid method\n    }\n\n    return RequestRewriteRule(\n        enabled: map['enabled'] == true,\n        name: map['name'],\n        url: map['url'] ?? map['domain'] + map['path'],\n        type: RuleType.fromName(map['type']),\n        rewritePath: map['rewritePath'],\n        method: method);\n  }\n\n  void updatePathReg() {\n    _urlReg = RegExp(url.replaceAll(\"*\", \".*\").replaceFirst('?', '\\\\?'));\n  }\n\n  Map<String, dynamic> toJson() {\n    var json = {\n      'name': name,\n      'enabled': enabled,\n      'url': url,\n      'type': type.name,\n      'rewritePath': rewritePath,\n    };\n\n    if (method != null) {\n      json['method'] = method!.name;\n    }\n\n    return json;\n  }\n}\n\nenum ReplaceBodyType {\n  text(\"文本\"),\n  file(\"文件\");\n\n  final String label;\n\n  const ReplaceBodyType(this.label);\n}\n\nclass RewriteItem {\n  bool enabled;\n  RewriteType type;\n\n  //key redirectUrl, method, path, queryParam, headers, body, statusCode\n  final Map<String, dynamic> values = {};\n\n  RewriteItem(this.type, this.enabled, {Map<dynamic, dynamic>? values}) {\n    if (values != null) {\n      this.values.addAll(Map.from(values));\n    }\n  }\n\n  factory RewriteItem.fromJson(Map<dynamic, dynamic> map) {\n    return RewriteItem(RewriteType.fromName(map['type']), map['enabled'], values: map['values']);\n  }\n\n  static List<RewriteItem> fromRequest(HttpRequest request) {\n    List<RewriteItem> items = [];\n    items.add(RewriteItem(RewriteType.replaceRequestLine, false)..path = request.requestUri?.path);\n    items.add(RewriteItem(RewriteType.replaceRequestHeader, false)..headers = request.headers.toMap());\n    items.add(RewriteItem(RewriteType.replaceRequestBody, true)..body = request.getBodyString());\n\n    return items;\n  }\n\n  static List<RewriteItem> fromResponse(HttpResponse response) {\n    List<RewriteItem> items = [];\n    items.add(RewriteItem(RewriteType.replaceResponseStatus, false)..statusCode = response.status.code);\n    items.add(RewriteItem(RewriteType.replaceResponseHeader, false)..headers = response.headers.toMap());\n    items.add(RewriteItem(RewriteType.replaceResponseBody, true)..body = response.getBodyString());\n\n    return items;\n  }\n\n  //key\n  String? get key => values['key'];\n\n  set key(String? key) => values['key'] = key;\n\n  String? get value => values['value'];\n\n  set value(String? value) => values['value'] = value;\n\n  //redirectUrl\n  String? get redirectUrl => values['redirectUrl'];\n\n  set redirectUrl(String? redirectUrl) => values['redirectUrl'] = redirectUrl;\n\n  //method\n  HttpMethod? get method => values['method'] == null\n      ? null\n      : HttpMethod.values.firstWhereOrNull((element) => element.name == values['method']);\n\n  set method(HttpMethod? method) => values['method'] = method?.name;\n\n  String? get path => values['path'];\n\n  set path(String? path) => values['path'] = path;\n\n  //queryParam\n  String? get queryParam => values['queryParam'];\n\n  set queryParam(String? queryParam) => values['queryParam'] = queryParam;\n\n  //statusCode\n  int? get statusCode => values['statusCode'];\n\n  set statusCode(int? statusCode) => values['statusCode'] = statusCode;\n\n  //headers\n  Map<String, String>? get headers => values['headers'] == null ? null : Map.from(values['headers']);\n\n  set headers(Map<String, String>? headers) => values['headers'] = headers;\n\n  //body\n  String? get body => values['body'];\n\n  set body(String? body) => values['body'] = body;\n\n  String? get bodyType => values['bodyType'];\n\n  set bodyType(String? bodyType) => values['bodyType'] = bodyType;\n\n  String? get bodyFile => values['bodyFile'];\n\n  set bodyFile(String? bodyFile) => values['bodyFile'] = bodyFile;\n\n  Map<String, dynamic> toJson() {\n    return {\n      'enabled': enabled,\n      'type': type.name,\n      'values': values,\n    };\n  }\n\n  @override\n  String toString() {\n    return toJson().toString();\n  }\n}\n\nenum RewriteType {\n  //重定向\n  redirect(\"重定向\"),\n\n  //替换请求\n  replaceRequestLine(\"请求行\"),\n  replaceRequestHeader(\"请求头\"),\n  replaceRequestBody(\"请求体\"),\n  replaceResponseStatus(\"状态码\"),\n  replaceResponseHeader(\"响应头\"),\n  replaceResponseBody(\"响应体\"),\n\n  //修改请求\n  updateBody(\"修改Body\"),\n  addQueryParam(\"添加参数\"),\n  removeQueryParam(\"删除参数\"),\n  updateQueryParam(\"修改参数\"),\n  addHeader(\"添加头部\"),\n  removeHeader(\"删除头部\"),\n  updateHeader(\"修改头部\"),\n  ;\n\n  static List<RewriteType> updateRequest = [\n    updateBody,\n    addQueryParam,\n    updateQueryParam,\n    removeQueryParam,\n    addHeader,\n    updateHeader,\n    removeHeader\n  ];\n\n  static List<RewriteType> updateResponse = [updateBody, addHeader, updateHeader, removeHeader];\n\n  final String label;\n\n  const RewriteType(this.label);\n\n  static RewriteType fromName(String name) {\n    return values.firstWhere((element) => element.name == name);\n  }\n\n  String getDescribe(bool isCN) {\n    if (isCN) {\n      return label;\n    }\n\n    return name.replaceFirst(\"replace\", \"\").replaceFirst(\"Query\", \"\");\n  }\n}\n"
  },
  {
    "path": "lib/network/components/manager/script_manager.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter_js/flutter_js.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/cache.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/random.dart';\nimport 'package:proxypin/ui/component/device.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:http/http.dart' as http;\n\nimport '../js/script_engine.dart';\n\n/// @author wanghongen\n/// 2023/10/06\n/// js脚本\nclass ScriptManager {\n  static String template = \"\"\"\n// e.g. Add/Update/Remove：Queries、Headers、Body\nasync function onRequest(context, request) {\n  console.log(request.url);\n  //Update or add Header\n  //request.headers[\"X-New-Headers\"] = \"My-Value\";\n  \n  // Update Body use fetch API request，具体文档可网上搜索fetch API\n  //request.body = await fetch('https://www.baidu.com/').then(response => response.text());\n  return request;\n}\n\n//You can modify the Response Data here before it goes to the client\nasync function onResponse(context, request, response) {\n  // response.statusCode = 200;\n\n  //var body = JSON.parse(response.body);\n  //body['key'] = \"value\";\n  //response.body = JSON.stringify(body);\n  return response;\n}\n  \"\"\";\n\n  static String separator = Platform.pathSeparator;\n  static ScriptManager? _instance;\n  bool enabled = true;\n  List<ScriptItem> list = [];\n\n  final ExpiringCache<ScriptItem, String> _scriptMap = ExpiringCache<ScriptItem, String>(Duration(minutes: 15));\n\n  static late JavascriptRuntime flutterJs;\n\n  static String? deviceId;\n\n  static final List<LogHandler> _logHandlers = [];\n\n  ScriptManager._();\n\n  ///单例\n  static Future<ScriptManager> get instance async {\n    if (_instance == null) {\n      _instance = ScriptManager._();\n      await _instance?.reloadScript();\n      flutterJs = await JavaScriptEngine.getJavaScript(consoleLog: consoleLog);\n      deviceId = await DeviceUtils.deviceId();\n\n      logger.d('init script manager $deviceId');\n    }\n    return _instance!;\n  }\n\n  static void registerConsoleLog(int fromWindowId) {\n    LogHandler logHandler = LogHandler(\n        channelId: fromWindowId,\n        handle: (logInfo) {\n          DesktopMultiWindow.invokeMethod(fromWindowId, \"consoleLog\", logInfo.toJson()).onError((e, t) {\n            logger.e(\"consoleLog error: $e\");\n            removeLogHandler(fromWindowId);\n          });\n        });\n    registerLogHandler(logHandler);\n  }\n\n  static void registerLogHandler(LogHandler logHandler) {\n    if (_logHandlers.any((it) => it.channelId == logHandler.channelId)) {\n       _logHandlers.removeWhere((it) => it.channelId == logHandler.channelId);\n    }\n    _logHandlers.add(logHandler);\n  }\n\n  static void removeLogHandler(int channelId) {\n    _logHandlers.removeWhere((element) => channelId == element.channelId);\n  }\n\n  static dynamic consoleLog(dynamic args) async {\n    if (_logHandlers.isEmpty) {\n      return;\n    }\n\n    var level = args.removeAt(0);\n    String output = args.join(' ');\n    if (level == 'info') level = 'warn';\n    LogInfo logInfo = LogInfo(level, output);\n    for (int i = 0; i < _logHandlers.length; i++) {\n      _logHandlers[i].handle.call(logInfo);\n    }\n  }\n\n  ///重新加载脚本\n  Future<void> reloadScript() async {\n    List<ScriptItem> scripts = [];\n    var file = await _path;\n    logger.d(\"reloadScript ${file.path}\");\n\n    if (await file.exists()) {\n      var content = await file.readAsString();\n      if (content.isEmpty) {\n        return;\n      }\n      var config = jsonDecode(content);\n      enabled = config['enabled'] == true;\n      for (var entry in config['list']) {\n        scripts.add(ScriptItem.fromJson(entry));\n      }\n    }\n    list = scripts;\n    _scriptMap.clear();\n  }\n\n  static String? _homePath;\n\n  static Future<String> homePath() async {\n    if (_homePath != null) {\n      return _homePath!;\n    }\n\n    if (Platform.isMacOS) {\n      _homePath = await DesktopMultiWindow.invokeMethod(0, \"getApplicationSupportDirectory\");\n    } else {\n      _homePath = await getApplicationSupportDirectory().then((it) => it.path);\n    }\n    return _homePath!;\n  }\n\n  static Future<File> get _path async {\n    final path = await homePath();\n    var file = File('$path${separator}script.json');\n    if (!await file.exists()) {\n      await file.create();\n    }\n    return file;\n  }\n\n  Future<String?> getScript(ScriptItem item) async {\n    // Local script (existing behavior)\n    if (_scriptMap.containsKey(item)) {\n      return _scriptMap[item]!;\n    }\n\n    // Remote script\n    if (item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty) {\n      var script = await _fetchRemoteScript(item);\n      if (script != null) {\n        _scriptMap[item] = script;\n      }\n      return script;\n    }\n\n    final home = await homePath();\n    var script = await File(home + item.scriptPath!).readAsString();\n    _scriptMap[item] = script;\n    return script;\n  }\n\n  Future<String?> _fetchRemoteScript(ScriptItem item) async {\n    final url = item.remoteUrl!.trim();\n    if (!_isHttpUrl(url)) {\n      return null;\n    }\n\n    final resp = await http.get(Uri.parse(url));\n\n    final bytes = resp.bodyBytes;\n\n    final content = utf8.decode(bytes);\n    _scriptMap[item] = content;\n\n    return content;\n  }\n\n  bool _isHttpUrl(String url) {\n    final uri = Uri.tryParse(url);\n    if (uri == null) return false;\n    return uri.scheme == 'http' || uri.scheme == 'https';\n  }\n\n  ///添加脚本\n  Future<void> addScript(ScriptItem item, String? script) async {\n    // Remote script: script is treated as initial cache (optional)\n    if (item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty) {\n      list.add(item);\n      return;\n    }\n\n    script ??= template;\n    final path = await homePath();\n    String scriptPath = \"${separator}scripts$separator${RandomUtil.randomString(16)}.js\";\n    var file = File(path + scriptPath);\n    await file.create(recursive: true);\n    file.writeAsString(script);\n    item.scriptPath = scriptPath;\n    list.add(item);\n    _scriptMap[item] = script;\n  }\n\n  ///更新脚本\n  Future<void> updateScript(ScriptItem item, String script) async {\n    // Remote scripts: update cache file (treat as local override of cache)\n    if (item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty) {\n      _scriptMap[item] = script;\n      return;\n    }\n\n    if (_scriptMap[item] == script) {\n      return;\n    }\n\n    final home = await homePath();\n    File(home + item.scriptPath!).writeAsString(script);\n    _scriptMap[item] = script;\n  }\n\n  ///删除脚本\n  Future<void> removeScript(int index) async {\n    var item = list.removeAt(index);\n    _scriptMap.remove(item);\n\n    if (item.scriptPath != null) {\n      final home = await homePath();\n      File(home + item.scriptPath!).delete();\n    }\n  }\n\n  Future<void> clean() async {\n    _scriptMap.clear();\n    while (list.isNotEmpty) {\n      var item = list.removeLast();\n      if (item.scriptPath != null) {\n        final home = await homePath();\n        File(home + item.scriptPath!).delete();\n      }\n    }\n    await flushConfig();\n  }\n\n  ///刷新配置\n  Future<void> flushConfig() async {\n    await _path.then((value) => value.writeAsString(jsonEncode({'enabled': enabled, 'list': list})));\n  }\n\n  Map<dynamic, dynamic> scriptSession = {};\n\n  ///脚本上下文\n  Map<String, dynamic> scriptContext(ScriptItem item) {\n    return {'scriptName': item.name, 'os': Platform.operatingSystem, 'session': scriptSession, \"deviceId\": deviceId};\n  }\n\n  ///运行脚本\n  Future<HttpRequest?> runScript(HttpRequest request) async {\n    if (!enabled) {\n      return request;\n    }\n    var url = request.domainPath;\n    for (var item in list) {\n      if (item.enabled && item.match(url)) {\n        var context = jsonEncode(scriptContext(item));\n        var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request));\n        String? script = await getScript(item);\n        if (script == null) {\n          continue;\n        }\n\n        var jsResult = await flutterJs.evaluateAsync(\n            \"\"\"var request = $jsRequest, context = $context;  request['scriptContext'] = context; $script\\n  onRequest(context, request)\"\"\");\n        var result = await JavaScriptEngine.jsResultResolve(flutterJs, jsResult);\n        if (result == null) {\n          return null;\n        }\n        request.attributes['scriptContext'] = result['scriptContext'];\n        scriptSession = result['scriptContext']['session'] ?? {};\n        request = JavaScriptEngine.convertHttpRequest(request, result);\n      }\n    }\n    return request;\n  }\n\n  ///运行脚本\n  Future<HttpResponse?> runResponseScript(HttpResponse response) async {\n    if (!enabled || response.request == null) {\n      return response;\n    }\n\n    var request = response.request!;\n    var url = request.domainPath;\n    for (var item in list) {\n      if (item.enabled && item.match(url)) {\n        var context = jsonEncode(request.attributes['scriptContext'] ?? scriptContext(item));\n        var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request));\n        var jsResponse = jsonEncode(await JavaScriptEngine.convertJsResponse(response));\n        String? script = await getScript(item);\n        if (script == null) {\n          continue;\n        }\n\n        var jsResult = await flutterJs.evaluateAsync(\n            \"\"\"var response = $jsResponse, context = $context;  response['scriptContext'] = context; $script\n            \\n  onResponse(context, $jsRequest, response);\"\"\");\n        // print(\"response: ${jsResult.isPromise} ${jsResult.isError} ${jsResult.rawResult}\");\n        var result = await JavaScriptEngine.jsResultResolve(flutterJs, jsResult);\n        if (result == null) {\n          return null;\n        }\n        scriptSession = result['scriptContext']['session'] ?? {};\n        response = JavaScriptEngine.convertHttpResponse(response, result);\n      }\n    }\n    return response;\n  }\n}\n\nclass LogHandler {\n  final int channelId;\n  final Function(LogInfo logInfo) handle;\n\n  LogHandler({required this.channelId, required this.handle});\n}\n\nclass LogInfo {\n  final DateTime time;\n  final String level;\n  final String output;\n\n  LogInfo(this.level, this.output, {DateTime? time}) : time = time ?? DateTime.now();\n\n  factory LogInfo.fromJson(Map<String, dynamic> json) {\n    return LogInfo(json['level'], json['output'], time: DateTime.fromMillisecondsSinceEpoch(json['time']));\n  }\n\n  Map<String, dynamic> toJson() {\n    return {'time': time.millisecondsSinceEpoch, 'level': level, 'output': output};\n  }\n\n  @override\n  String toString() {\n    return '{time: $time, level: $level, output: $output}';\n  }\n}\n\nclass ScriptItem {\n  bool enabled = true;\n  String? name;\n  List<String> urls;\n  String? scriptPath;\n  List<RegExp?>? urlRegs;\n\n  String? remoteUrl;\n\n  ScriptItem(this.enabled, this.name, dynamic urls, {this.scriptPath, this.remoteUrl})\n      : urls = urls is String\n            ? (urls.contains(',') ? urls.split(',').map((e) => e.trim()).toList() : [urls])\n            : (urls is List<String> ? urls : <String>[]);\n\n  // 匹配url，任意一个规则匹配即可\n  bool match(String url) {\n    urlRegs ??= urls.map((u) => RegExp(u.replaceAll(\"*\", \".*\"))).toList();\n    for (final reg in urlRegs!) {\n      if (reg!.hasMatch(url)) return true;\n    }\n    return false;\n  }\n\n  factory ScriptItem.fromJson(Map<dynamic, dynamic> json) {\n    final urlField = json['url'];\n    List<String> urls;\n    if (urlField is List) {\n      urls = urlField.cast<String>();\n    } else if (urlField is String) {\n      urls = urlField.contains(',') ? urlField.split(',').map((e) => e.trim()).toList() : [urlField];\n    } else {\n      urls = <String>[];\n    }\n\n    return ScriptItem(\n      json['enabled'],\n      json['name'],\n      urls,\n      scriptPath: json['scriptPath'],\n      remoteUrl: json['remoteUrl'],\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'enabled': enabled,\n      'name': name,\n      'url': urls.length == 1 ? urls[0] : urls,\n      'scriptPath': scriptPath,\n      if (remoteUrl != null) 'remoteUrl': remoteUrl,\n    };\n  }\n\n  @override\n  String toString() {\n    return 'ScriptItem{enabled: $enabled, name: $name, url: $urls, scriptPath: $scriptPath, remoteUrl: $remoteUrl}';\n  }\n}\n"
  },
  {
    "path": "lib/network/components/report_server_interceptor.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:proxypin/network/util/compress.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/utils/har.dart';\n\nimport '../http/http.dart';\nimport 'interceptor.dart';\nimport 'manager/report_server_manager.dart';\n\n/// Hosts interceptor\n/// @author wanghongen\nclass ReportServerInterceptor extends Interceptor {\n  Future<ReportServerManager> get reportServerManager async => await ReportServerManager.instance;\n\n  static HttpClient httpClient = HttpClient();\n\n  @override\n  int get priority => 1000;\n\n  @override\n  Future<HttpResponse?> onResponse(HttpRequest request, HttpResponse response) async {\n    // Fire-and-forget reporting; don't block the proxy pipeline\n    unawaited(reportServer(request, response));\n    return response;\n  }\n\n  @override\n  Future<void> onError(HttpRequest? request, error, StackTrace? stackTrace) async {\n    if (request != null) {\n      unawaited(reportServer(request, null, error: error, stackTrace: stackTrace));\n    }\n    return;\n  }\n\n  Future<void> reportServer(HttpRequest request, HttpResponse? response,\n      {dynamic error, StackTrace? stackTrace}) async {\n    String requestUrl = request.requestUrl;\n    var manager = await reportServerManager;\n    var server = await manager.matchServer(requestUrl);\n    if (server == null) {\n      return;\n    }\n\n    try {\n      logger.i(\"reportServer start: $requestUrl -> ${server.name} (${server.serverUrl})\");\n\n      // Prepare server URL (ensure scheme)\n      var serverUrl = (server.serverUrl).trim();\n      if (serverUrl.isEmpty) {\n        logger.w('reportServer skipped: serverUrl empty for ${server.name}');\n        return;\n      }\n      if (!serverUrl.startsWith('http://') && !serverUrl.startsWith('https://')) {\n        serverUrl = 'http://$serverUrl';\n      }\n\n      final uri = Uri.parse(serverUrl);\n\n      var payload = Har.toHar(request);\n\n      List<int> body = utf8.encode(jsonEncode(payload));\n      // Apply compression if configured\n      final compression = server.compression?.toLowerCase();\n      if (compression == 'gzip') {\n        try {\n          body = gzipEncode(body);\n        } catch (e) {\n          logger.w('reportServer gzip compress failed: $e');\n        }\n      }\n\n      // Send POST\n      final ioReq = await httpClient.postUrl(uri).timeout(const Duration(seconds: 5));\n\n      // Set headers\n      final matchedRule = server.name;\n      if (matchedRule.isNotEmpty) {\n        ioReq.headers.set('X-Report-Name', matchedRule);\n      }\n\n      ioReq.headers.set(HttpHeaders.contentTypeHeader, 'application/json; charset=utf-8');\n      if (compression == 'gzip') {\n        ioReq.headers.set(HttpHeaders.contentEncodingHeader, 'gzip');\n      }\n\n      // Write body and close\n      ioReq.add(body);\n      final ioResp = await ioReq.close().timeout(const Duration(seconds: 30));\n      final respText = await ioResp.transform(utf8.decoder).join();\n      if (ioResp.statusCode >= 200 && ioResp.statusCode < 300) {\n        logger.i('reportServer delivered to ${server.name} (${uri.toString()}), status=${ioResp.statusCode}');\n      } else {\n        logger.w('reportServer delivery to ${server.name} failed, status=${ioResp.statusCode}, body=$respText');\n      }\n    } catch (e, st) {\n      logger.e(\"reportServer error $requestUrl\", error: e, stackTrace: st);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/network/components/request_block.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'package:proxypin/network/components/manager/request_block_manager.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nimport 'interceptor.dart';\n\n/// RequestBlockInterceptor is a component that can block the request or response.\n/// @author Hongen Wang\nclass RequestBlockInterceptor extends Interceptor {\n  @override\n  int get priority => 1000;\n\n  @override\n  Future<HttpRequest?> onRequest(HttpRequest request) async {\n    var uri = request.domainPath;\n    var blockRequest = (await RequestBlockManager.instance).enableBlockRequest(uri);\n    if (blockRequest) {\n      logger.d(\"[${request.requestId}] 屏蔽请求 $uri\");\n      return null;\n    }\n    return request;\n  }\n\n  @override\n  Future<HttpResponse?> onResponse(HttpRequest request, HttpResponse response) async {\n    var uri = request.domainPath;\n    var blockResponse = (await RequestBlockManager.instance).enableBlockResponse(uri);\n    if (blockResponse) {\n      logger.d(\"[${request.requestId}] 屏蔽响应 $uri\");\n      return null;\n    }\n    return response;\n  }\n}\n"
  },
  {
    "path": "lib/network/components/request_breakpoint.dart",
    "content": "import 'dart:async';\n\nimport 'package:proxypin/network/components/interceptor.dart';\nimport 'package:proxypin/network/components/manager/request_breakpoint_manager.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/cache.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/multi_window.dart';\n\nimport '../http/http_headers.dart';\n\nclass RequestBreakpointInterceptor extends Interceptor {\n  static RequestBreakpointInterceptor instance = RequestBreakpointInterceptor._();\n\n  final manager = RequestBreakpointManager.instance;\n\n  final ExpiringCache<String, Completer<HttpRequest?>> _pausedRequests = ExpiringCache(Duration(minutes: 10));\n  final ExpiringCache<String, Completer<HttpResponse?>> _pausedResponses = ExpiringCache(Duration(minutes: 10));\n\n  RequestBreakpointInterceptor._();\n\n  @override\n  Future<HttpRequest?> onRequest(HttpRequest request) async {\n    RequestBreakpointManager requestBreakpointManager = await manager;\n    if (!requestBreakpointManager.enabled) return request;\n\n    var url = request.requestUrl;\n    for (var rule in requestBreakpointManager.list) {\n      if (rule.match(url, method: request.method) && rule.interceptRequest) {\n        Completer<HttpRequest?> completer = Completer();\n        _pausedRequests[request.requestId] = completer;\n\n        // Open Breakpoint Executor Window\n        MultiWindow.openWindow(\"Breakpoint - Request\", 'BreakpointExecutor',\n            args: {'type': 'request', 'request': request.toJson(), 'requestId': request.requestId});\n\n        return completer.future.then((req) {\n          if (req == null) {\n            logger.d('Request ${request.requestId} was resumed null, aborting request');\n            return null;\n          }\n\n          request.method = req.method;\n          Uri uri = req.requestUri!;\n          if (uri.isScheme('https')) {\n            request.uri = uri.path + (uri.hasQuery ? \"?${uri.query}\" : \"\");\n          } else {\n            request.uri = uri.toString();\n          }\n\n          request.headers.clear();\n          request.headers.addAll(req.headers);\n          request.headers.remove(HttpHeaders.CONTENT_ENCODING);\n\n          request.body = req.body;\n          logger.d('Resuming request ${request.requestId} with modified request');\n          return request;\n        });\n      }\n    }\n    return request;\n  }\n\n  @override\n  Future<HttpResponse?> onResponse(HttpRequest request, HttpResponse response) async {\n    RequestBreakpointManager requestBreakpointManager = await manager;\n    if (!requestBreakpointManager.enabled) return response;\n\n    var url = request.requestUrl;\n    for (var rule in requestBreakpointManager.list) {\n      if (rule.match(url, method: request.method) && rule.interceptResponse) {\n        Completer<HttpResponse?> completer = Completer();\n        _pausedResponses[request.requestId] = completer;\n\n        // Open Breakpoint Executor Window\n        MultiWindow.openWindow(\"Breakpoint - Response\", 'BreakpointExecutor', args: {\n          'type': 'response',\n          'request': request.toJson(),\n          'response': response.toJson(),\n          'requestId': request.requestId\n        });\n\n        return completer.future.then((res) {\n          if (res == null) {\n            return null;\n          }\n\n          response.status = res.status;\n          response.headers.clear();\n          response.headers.addAll(res.headers);\n          response.headers.remove(HttpHeaders.CONTENT_ENCODING);\n\n          response.body = res.body;\n\n          logger.d('Resuming response for request ${request.requestId} with modified response');\n          return response;\n        });\n      }\n    }\n    return response;\n  }\n\n  void resumeRequest(String requestId, HttpRequest? request) {\n    if (_pausedRequests.containsKey(requestId)) {\n      _pausedRequests.remove(requestId)?.complete(request);\n    }\n  }\n\n  void resumeResponse(String requestId, HttpResponse? response) {\n    if (_pausedResponses.containsKey(requestId)) {\n      _pausedResponses.remove(requestId)?.complete(response);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/network/components/request_map.dart",
    "content": "/*\n * Copyright 2025 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter_js/flutter_js.dart';\nimport 'package:proxypin/network/components/interceptor.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/file_read.dart';\n\nimport 'js/script_engine.dart';\nimport 'manager/request_map_manager.dart';\nimport 'manager/script_manager.dart';\n\n///  RequestRewriteComponent is a component that can rewrite the request before sending it to the server.\n/// @author Hongen Wang\nclass RequestMapInterceptor extends Interceptor {\n  static RequestMapInterceptor instance = RequestMapInterceptor._();\n  static JavascriptRuntime? flutterJs;\n  static Map<dynamic, dynamic> scriptSession = {};\n\n  final managerInstance = RequestMapManager.instance;\n\n  RequestMapInterceptor._();\n\n  ///脚本上下文\n  Map<String, dynamic> scriptContext(RequestMapRule rule) {\n    return {'scriptName': rule.name, 'os': Platform.operatingSystem, 'session': scriptSession};\n  }\n\n  @override\n  Future<HttpResponse?> execute(HttpRequest request) async {\n    final manager = await managerInstance;\n    if (!manager.enabled) {\n      return null;\n    }\n    RequestMapRule? mapRule = manager.findMatch(request.requestUrl);\n    if (mapRule == null) {\n      return null;\n    }\n    var item = await manager.getMapItem(mapRule);\n    if (item == null) {\n      return null;\n    }\n\n    HttpResponse? response;\n    if (mapRule.type == RequestMapType.local) {\n      // 本地映射\n      response = await mapLocalResponse(mapRule, item);\n    } else if (mapRule.type == RequestMapType.script && item.script != null) {\n      response = await executeScript(request, mapRule, item.script!);\n    }\n\n    if (response == null) {\n      return null;\n    }\n\n    response.request = request;\n    request.response = response;\n    return response;\n  }\n\n  /// 重写响应\n  Future<HttpResponse> mapLocalResponse(RequestMapRule rule, RequestMapItem item) async {\n    HttpResponse response = HttpResponse(HttpStatus.valueOf(item.statusCode ?? 200));\n    item.headers?.forEach((key, value) {\n      response.headers.set(key, value);\n    });\n    if (item.bodyType == MapBodyType.file.name) {\n      if (item.bodyFile == null) return response;\n      response.body = await FileRead.readFile(item.bodyFile!);\n    } else if (item.body != null) {\n      response.body =\n          response.charset == 'utf-8' || response.charset == 'utf8' ? utf8.encode(item.body!) : item.body?.codeUnits;\n    }\n    return response;\n  }\n\n  /// script执行\n  Future<HttpResponse?> executeScript(HttpRequest request, RequestMapRule rule, String script) async {\n    flutterJs ??= await JavaScriptEngine.getJavaScript(consoleLog: ScriptManager.consoleLog);\n    var context = jsonEncode(scriptContext(rule));\n    var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request));\n\n    var jsResult = await flutterJs!.evaluateAsync(\n        \"\"\"var request = $jsRequest, context = $context;  request['scriptContext'] = context; $script\\n  onRequest(context, request)\"\"\");\n    // print(\"response: ${jsResult.isPromise} ${jsResult.isError} ${jsResult.rawResult}\");\n    var result = await JavaScriptEngine.jsResultResolve(flutterJs!, jsResult);\n    if (result == null) {\n      return null;\n    }\n\n    if (result['scriptContext']?['session'] != null) {\n      scriptSession = result['scriptContext']['session'];\n    }\n    HttpResponse response = HttpResponse(HttpStatus.valueOf(200));\n    response = JavaScriptEngine.convertHttpResponse(response, result);\n    return response;\n  }\n}\n"
  },
  {
    "path": "lib/network/components/request_rewrite.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'dart:collection';\nimport 'dart:convert';\n\nimport 'package:proxypin/network/components/interceptor.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/http/constants.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\nimport 'package:proxypin/network/util/file_read.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/uri.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nimport 'manager/rewrite_rule.dart';\n\n///  RequestRewriteComponent is a component that can rewrite the request before sending it to the server.\n/// @author Hongen Wang\nclass RequestRewriteInterceptor extends Interceptor {\n  static RequestRewriteInterceptor instance = RequestRewriteInterceptor._();\n\n  final requestRewriteManager = RequestRewriteManager.instance;\n\n  RequestRewriteInterceptor._();\n\n  @override\n  Future<HttpRequest?> onRequest(HttpRequest request) async {\n    //重写请求\n    var url = request.requestUrl;\n    await requestRewrite(url, request);\n    return request;\n  }\n\n  @override\n  Future<HttpResponse?> onResponse(HttpRequest request, HttpResponse response) async {\n    //重写响应\n    try {\n      var url = request.requestUrl;\n      await responseRewrite(url, response);\n    } catch (e, t) {\n      response.body = \"$e\".codeUnits;\n      logger.e('[${request.requestId}] 响应重写异常 ', error: e, stackTrace: t);\n    }\n    return response;\n  }\n\n  ///获取重定向\n  Future<String?> getRedirectRule(String? url) async {\n    var manager = await requestRewriteManager;\n    var rewriteRule = manager.getRewriteRule(url, [RuleType.redirect]);\n    if (rewriteRule == null) {\n      return null;\n    }\n\n    var rewriteItems = await manager.getRewriteItems(rewriteRule);\n    var redirectUrl = rewriteItems?.firstWhereOrNull((element) => element.enabled)?.redirectUrl;\n    if (rewriteRule.url.contains(\"*\") && redirectUrl?.contains(\"*\") == true) {\n      String ruleUrl = rewriteRule.url.replaceAll(\"*\", \"\");\n      redirectUrl = redirectUrl?.replaceAll(\"*\", url!.replaceAll(ruleUrl, \"\"));\n    }\n    return redirectUrl;\n  }\n\n  /// 重写请求\n  Future<void> requestRewrite(String url, HttpRequest request) async {\n    var manager = await RequestRewriteManager.instance;\n    var rewriteRule = manager.getRewriteRule(url, [RuleType.requestReplace, RuleType.requestUpdate]);\n\n    if (rewriteRule?.type == RuleType.requestReplace) {\n      var rewriteItems = await manager.getRewriteItems(rewriteRule!);\n      for (var item in rewriteItems!) {\n        if (item.enabled) {\n          await _replaceRequest(request, item);\n        }\n      }\n    }\n\n    if (rewriteRule?.type == RuleType.requestUpdate) {\n      var rewriteItems = await manager.getRewriteItems(rewriteRule!);\n      if (rewriteItems == null) {\n        return;\n      }\n      for (var item in rewriteItems) {\n        if (item.enabled) {\n          await _updateRequest(request, item);\n        }\n      }\n    }\n  }\n\n  /// 重写响应\n  Future<void> responseRewrite(String? url, HttpResponse response) async {\n    var manager = await RequestRewriteManager.instance;\n\n    var rewriteRule = manager.getRewriteRule(url, [RuleType.responseReplace, RuleType.responseUpdate]);\n    if (rewriteRule == null) {\n      return;\n    }\n\n    if (rewriteRule.type == RuleType.responseReplace) {\n      var rewriteItems = await manager.getRewriteItems(rewriteRule);\n      for (var item in rewriteItems!) {\n        if (item.enabled) {\n          await _replaceResponse(response, item);\n        }\n      }\n    }\n\n    if (rewriteRule.type == RuleType.responseUpdate) {\n      var rewriteItems = await manager.getRewriteItems(rewriteRule);\n      if (rewriteItems == null) {\n        return;\n      }\n\n      for (var item in rewriteItems) {\n        if (item.enabled) {\n          await _updateMessage(response, item);\n        }\n      }\n    }\n  }\n\n  Future<void> _updateRequest(HttpRequest request, RewriteItem item) async {\n    var paramTypes = [RewriteType.addQueryParam, RewriteType.removeQueryParam, RewriteType.updateQueryParam];\n\n    if (paramTypes.contains(item.type)) {\n      var requestUri = request.requestUri;\n      Map<String, dynamic> queryParameters = LinkedHashMap.from(requestUri!.queryParameters);\n\n      switch (item.type) {\n        case RewriteType.addQueryParam:\n          queryParameters[item.key!] = item.value;\n          break;\n        case RewriteType.removeQueryParam:\n          if (item.value?.trim().isNotEmpty == true) {\n            var val = queryParameters[item.key!];\n            if (val == null || !RegExp(item.value!).hasMatch(val)) {\n              break;\n            }\n          }\n          queryParameters.remove(item.key!);\n          break;\n        case RewriteType.updateQueryParam:\n          var itemKey = item.key;\n          if (itemKey == null || itemKey.trim().isEmpty) return;\n\n          var entries = Map.of(queryParameters).entries;\n          var regExp = RegExp(item.key!);\n\n          for (var entry in entries) {\n            var line = \"${entry.key}=${entry.value}\";\n\n            if (regExp.hasMatch(line)) {\n              line = line.replaceAll(regExp, item.value ?? '');\n              var pair = line.splitFirst(HttpConstants.equal);\n              if (pair.first != entry.key) queryParameters.remove(entry.key);\n\n              queryParameters[pair.first] = pair.length > 1 ? pair.last : '';\n              break;\n            }\n          }\n          break;\n        default:\n          break;\n      }\n      requestUri = requestUri.replace(query: UriUtils.mapToQuery(queryParameters));\n      if (requestUri.isScheme('https')) {\n        request.uri = requestUri.path + (requestUri.hasQuery ? \"?${requestUri.query}\" : \"\");\n      } else {\n        request.uri = requestUri.toString();\n      }\n      return;\n    }\n\n    await _updateMessage(request, item);\n  }\n\n  //修改消息\n  Future<void> _updateMessage(HttpMessage message, RewriteItem item) async {\n    if (item.type == RewriteType.updateBody && message.body != null) {\n      String body = (await message.decodeBodyString()).replaceAllMapped(RegExp(item.key!), (match) {\n        if (match.groupCount > 0 && item.value?.contains(\"\\$1\") == true) {\n          return item.value!.replaceAll(\"\\$1\", match.group(1)!);\n        }\n        return item.value ?? '';\n      });\n\n      message.body = message.charset == 'utf-8' || message.charset == 'utf8' ? utf8.encode(body) : body.codeUnits;\n\n      message.headers.remove(HttpHeaders.CONTENT_ENCODING);\n      message.headers.contentLength = message.body!.length;\n      return;\n    }\n\n    if (item.type == RewriteType.addHeader) {\n      message.headers.set(item.key!, item.value ?? '');\n      return;\n    }\n\n    if (item.type == RewriteType.removeHeader) {\n      if (item.value?.trim().isNotEmpty == true) {\n        var val = message.headers.get(item.key!);\n        if (val == null || !RegExp(item.value!).hasMatch(val)) {\n          return;\n        }\n      }\n      message.headers.remove(item.key!);\n      return;\n    }\n\n    if (item.type == RewriteType.updateHeader) {\n      if (item.key == null || item.key?.trim().isEmpty == true) return;\n\n      var headers = Map.of(message.headers.getHeaders());\n      var regExp = RegExp(item.key!, caseSensitive: false);\n\n      headers.forEach((key, values) {\n        var line = \"$key: ${values.firstOrNull ?? ''}\";\n        if (regExp.hasMatch(line)) {\n          line = line.replaceAll(regExp, item.value ?? '');\n          var pair = line.splitFirst(HttpConstants.colon);\n          if (pair.first != key) message.headers.remove(key);\n          message.headers.set(pair.first, pair.length > 1 ? pair.last : '');\n        }\n      });\n      return;\n    }\n  }\n\n  //替换请求\n  Future<void> _replaceRequest(HttpRequest request, RewriteItem item) async {\n    if (item.type == RewriteType.replaceRequestLine) {\n      request.method = item.method ?? request.method;\n      Uri uri = Uri.parse(request.requestUrl).replace(path: item.path, query: item.queryParam);\n      if (uri.isScheme('https')) {\n        request.uri = uri.path + (uri.hasQuery ? \"?${uri.query}\" : \"\");\n      } else {\n        request.uri = uri.toString();\n      }\n      return;\n    }\n    await _replaceHttpMessage(request, item);\n  }\n\n  //替换相应\n  Future<void> _replaceResponse(HttpResponse response, RewriteItem item) async {\n    if (item.type == RewriteType.replaceResponseStatus && item.statusCode != null) {\n      response.status = HttpStatus.valueOf(item.statusCode!);\n      return;\n    }\n    await _replaceHttpMessage(response, item);\n  }\n\n  Future<void> _replaceHttpMessage(HttpMessage message, RewriteItem item) async {\n    if ((item.type == RewriteType.replaceRequestHeader || item.type == RewriteType.replaceResponseHeader) &&\n        item.headers != null) {\n      item.headers?.forEach((key, value) => message.headers.set(key, value));\n      return;\n    }\n\n    if (item.type == RewriteType.replaceResponseBody || item.type == RewriteType.replaceRequestBody) {\n      if (item.bodyType == ReplaceBodyType.file.name) {\n        if (item.bodyFile == null) return;\n\n        message.body = await FileRead.readFile(item.bodyFile!);\n        message.headers.contentLength = message.body!.length;\n        message.headers.remove(HttpHeaders.CONTENT_ENCODING);\n        return;\n      }\n\n      if (item.body != null) {\n        message.body =\n            message.charset == 'utf-8' || message.charset == 'utf8' ? utf8.encode(item.body!) : item.body?.codeUnits;\n        message.headers.contentLength = message.body!.length;\n        message.headers.remove(HttpHeaders.CONTENT_ENCODING);\n      }\n      return;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/network/components/script.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'package:proxypin/network/components/interceptor.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nimport 'manager/script_manager.dart';\n\n///  developers can write JS code to flexibly manipulate requests/responses\n///@author Hongen Wang\nclass ScriptInterceptor extends Interceptor {\n  @override\n  int get priority => 10;\n\n  @override\n  Future<HttpRequest?> onRequest(HttpRequest request) async {\n    //脚本替换\n    var scriptManager = await ScriptManager.instance;\n    HttpRequest? httpRequest = await scriptManager.runScript(request);\n    if (httpRequest == null) {\n      return null;\n    }\n    return request;\n  }\n\n  @override\n  Future<HttpResponse?> onResponse(HttpRequest request, HttpResponse response) async {\n    //脚本替换\n    var scriptManager = await ScriptManager.instance;\n    try {\n      HttpResponse? httpResponse = await scriptManager.runResponseScript(response);\n      if (httpResponse == null) {\n        return null;\n      }\n      return httpResponse;\n    } catch (e, t) {\n      response.status = HttpStatus(-1, 'Script exec error');\n      response.body = \"$e\\n${response.bodyAsString}\".codeUnits;\n      logger.e('[${request.requestId}] 执行脚本异常 ', error: e, stackTrace: t);\n    }\n    return response;\n  }\n}\n"
  },
  {
    "path": "lib/network/handle/http_proxy_handle.dart",
    "content": "import 'dart:convert';\n\nimport 'package:proxypin/network/bin/listener.dart';\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/components/host_filter.dart';\nimport 'package:proxypin/network/components/interceptor.dart';\nimport 'package:proxypin/network/components/request_rewrite.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/proxy_helper.dart';\nimport 'package:proxypin/network/util/attribute_keys.dart';\nimport 'package:proxypin/network/util/uri.dart';\nimport 'package:proxypin/utils/ip.dart';\n\n/// http请求处理器\nclass HttpProxyChannelHandler extends ChannelHandler<HttpRequest> {\n  EventListener? listener;\n\n  final List<Interceptor> interceptors;\n\n  HttpProxyChannelHandler({this.listener, required this.interceptors});\n\n  @override\n  Future<void> channelRead(ChannelContext channelContext, Channel channel, HttpRequest msg) async {\n    //下载证书\n    if (msg.uri == 'http://proxy.pin/ssl' || msg.requestUrl == 'http://127.0.0.1:${channel.socket.port}/ssl') {\n      ProxyHelper.crtDownload(channelContext, channel, msg);\n      return;\n    }\n    //请求本服务\n    if (((await localIps()).contains(msg.hostAndPort?.host) || '127.0.0.1' == msg.hostAndPort?.host) &&\n        msg.hostAndPort?.port == channel.socket.port) {\n      ProxyHelper.localRequest(channelContext, msg, channel);\n      return;\n    }\n\n    //代理转发请求\n    try {\n      await forward(channelContext, channel, msg).catchError((error, trace) {\n        exceptionCaught(channelContext, channel, error, trace: trace);\n      });\n    } catch (error, trace) {\n      exceptionCaught(channelContext, channel, error, trace: trace);\n    }\n  }\n\n  @override\n  void exceptionCaught(ChannelContext channelContext, Channel channel, error, {StackTrace? trace}) {\n    super.exceptionCaught(channelContext, channel, error, trace: trace);\n    ProxyHelper.exceptionHandler(channelContext, channel, listener, channelContext.currentRequest, error);\n    for (var interceptor in interceptors) {\n      interceptor.onError(channelContext.currentRequest, error, trace);\n    }\n  }\n\n  @override\n  void channelInactive(ChannelContext channelContext, Channel channel) {\n    Channel? remoteChannel = channelContext.serverChannel;\n    remoteChannel?.close();\n    // log.d(\"[${channel.id}] close  ${channel.error}\");\n  }\n\n  /// 转发请求\n  Future<void> forward(ChannelContext channelContext, Channel channel, HttpRequest httpRequest) async {\n    // log.d(\"[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}\");\n    if (channel.error != null) {\n      ProxyHelper.exceptionHandler(channelContext, channel, listener, httpRequest, channel.error);\n      return;\n    }\n\n    //获取远程连接\n    Channel remoteChannel;\n    try {\n      remoteChannel = await _getRemoteChannel(channelContext, channel, httpRequest);\n    } catch (error, stackTrace) {\n      log.e(\"[${channel.id}] 连接异常 ${httpRequest.method.name} ${httpRequest.requestUrl}\",\n          error: error, stackTrace: stackTrace);\n      if (httpRequest.method == HttpMethod.connect) {\n        channel.error = error; //记录异常\n        //https代理新建connect连接请求 返回ok 会继续发起正常请求 可以获取到请求内容\n        await channel.write(channelContext,\n            HttpResponse(HttpStatus.ok.reason('Connection established'), protocolVersion: httpRequest.protocolVersion));\n      } else {\n        rethrow;\n      }\n      return;\n    }\n\n    //实现抓包代理转发\n    if (httpRequest.method != HttpMethod.connect) {\n      // log.d(\n      //     \"[${channel.id}] streamId:${httpRequest.streamId ?? ''} ${httpRequest.protocolVersion}  ${httpRequest.method.name} ${httpRequest.requestUrl}\");\n      if (HostFilter.filter(httpRequest.hostAndPort?.host)) {\n        await remoteChannel.write(channelContext, httpRequest);\n        return;\n      }\n\n      HttpRequest? request = httpRequest;\n\n      //拦截器\n      for (var interceptor in interceptors) {\n        request = await interceptor.onRequest(request!);\n        if (request == null) {\n          listener?.onRequest(channel, httpRequest);\n          channel.close();\n          remoteChannel.close();\n          return;\n        }\n      }\n      channelContext.currentRequest = request;\n\n      listener?.onRequest(channel, request!);\n\n      for (var interceptor in interceptors) {\n        var response = await interceptor.execute(request!);\n        if (response != null) {\n          listener?.onResponse(channelContext, response);\n          channel.writeAndClose(channelContext, response);\n          return;\n        }\n      }\n\n      //重定向\n      var uri = request!.domainPath;\n      String? redirectUrl = await (RequestRewriteInterceptor.instance).getRedirectRule(uri);\n      if (redirectUrl?.isNotEmpty == true) {\n        await redirect(channelContext, channel, request, redirectUrl!);\n        return;\n      }\n\n      //http1 直接请求  不需要携带域名\n      if (!remoteChannel.useProxy &&\n          request.protocolVersion == HttpMessage.http1Version &&\n          request.uri.startsWith(HostAndPort.httpScheme)) {\n        final requestUri = request.requestUri!;\n        request.uri = \"${requestUri.path}${requestUri.hasQuery ? '?${requestUri.query}' : ''}\";\n      }\n      await remoteChannel.write(channelContext, request);\n    }\n  }\n\n  //重定向\n  Future<void> redirect(\n      ChannelContext channelContext, Channel channel, HttpRequest httpRequest, String redirectUrl) async {\n    var proxyHandler = HttpResponseProxyHandler(channel, interceptors, listener: listener);\n\n    var redirectUri = UriBuild.build(redirectUrl, params: httpRequest.queries.isEmpty ? null : httpRequest.queries);\n    log.d(\"[${channel.id}] 重定向 $redirectUri\");\n\n    if (redirectUri.isScheme('https')) {\n      httpRequest.uri = redirectUri.path + (redirectUri.hasQuery ? '?${redirectUri.query}' : '');\n    } else {\n      httpRequest.uri = redirectUri.toString();\n    }\n    httpRequest.headers.host = '${redirectUri.host}${redirectUri.hasPort ? ':${redirectUri.port}' : ''}';\n    var redirectChannel = await HttpClients.connect(redirectUri, proxyHandler, channelContext);\n    channelContext.serverChannel = redirectChannel;\n    await redirectChannel.write(channelContext, httpRequest);\n  }\n\n  /// 获取远程连接\n  Future<Channel> _getRemoteChannel(\n      ChannelContext channelContext, Channel clientChannel, HttpRequest httpRequest) async {\n    //客户端连接 作为缓存\n    Channel? remoteChannel = channelContext.serverChannel;\n    if (remoteChannel != null) {\n      return remoteChannel;\n    }\n\n    var hostAndPort = httpRequest.hostAndPort ?? getHostAndPort(httpRequest);\n    channelContext.host = hostAndPort;\n\n    //远程转发\n    HostAndPort? remote = channelContext.getAttribute(AttributeKeys.remote);\n    //外部代理\n    ProxyInfo? proxyInfo = channelContext.getAttribute(AttributeKeys.proxyInfo);\n\n    if (remote != null || proxyInfo != null) {\n      HostAndPort connectHost = remote ?? HostAndPort.host(proxyInfo!.host, proxyInfo.port!);\n      final proxyChannel = await connectRemote(channelContext, clientChannel, connectHost);\n      proxyChannel.useProxy = true;\n\n      //代理建立完连接判断是否是https 需要发起connect请求\n      if (httpRequest.method == HttpMethod.connect) {\n        //proxy Authorization\n        if (proxyInfo?.isAuthenticated == true) {\n          String auth = base64Encode(utf8.encode(\"${proxyInfo?.username}:${proxyInfo?.password}\"));\n          httpRequest.headers.set(HttpHeaders.PROXY_AUTHORIZATION, 'Basic $auth');\n        }\n\n        await proxyChannel.write(channelContext, httpRequest);\n      } else {\n        if (clientChannel.isSsl) {\n          await HttpClients.connectRequest(channelContext, hostAndPort, proxyChannel, proxyInfo: proxyInfo);\n          await proxyChannel.secureSocket(channelContext,\n              host: hostAndPort.host, supportedProtocols: httpRequest.protocolVersion == \"HTTP/2\" ? [\"h2\"] : null);\n        }\n      }\n\n      return proxyChannel;\n    }\n\n    HostAndPort remoteAddress = hostAndPort;\n\n    final ProxyInfo? socksProxy = channelContext.getAttribute(AttributeKeys.socks5Proxy);\n    if (socksProxy != null) {\n      remoteAddress = hostAndPort.copyWith(host: socksProxy.host, port: socksProxy.port!);\n    }\n\n    for (var interceptor in interceptors) {\n      remoteAddress = await interceptor.preConnect(remoteAddress);\n    }\n\n    final proxyChannel = await connectRemote(channelContext, clientChannel, remoteAddress);\n    if (clientChannel.isSsl) {\n      await proxyChannel.secureSocket(channelContext,\n          host: hostAndPort.host,\n          supportedProtocols: channelContext.clientChannel?.selectedProtocol == null\n              ? null\n              : [channelContext.clientChannel!.selectedProtocol!]);\n    }\n\n    //https代理新建连接请求\n    if (httpRequest.method == HttpMethod.connect) {\n      await clientChannel.write(channelContext,\n          HttpResponse(HttpStatus.ok.reason('Connection established'), protocolVersion: httpRequest.protocolVersion));\n    }\n    return proxyChannel;\n  }\n\n  /// 连接远程\n  Future<Channel> connectRemote(ChannelContext channelContext, Channel clientChannel, HostAndPort connectHost) async {\n    var proxyHandler = HttpResponseProxyHandler(clientChannel, interceptors, listener: listener);\n    var proxyChannel = await channelContext.connectServerChannel(connectHost, proxyHandler);\n    return proxyChannel;\n  }\n}\n\n/// http响应代理\nclass HttpResponseProxyHandler extends ChannelHandler<HttpResponse> {\n  //客户端的连接\n  final Channel clientChannel;\n\n  EventListener? listener;\n  final List<Interceptor> interceptors;\n\n  HttpResponseProxyHandler(this.clientChannel, this.interceptors, {this.listener});\n\n  @override\n  Future<void> channelRead(ChannelContext channelContext, Channel channel, HttpResponse msg) async {\n    var request = msg.request ?? channelContext.currentRequest;\n    request?.response = msg;\n\n    //域名是否过滤\n    if (HostFilter.filter(request?.hostAndPort?.host) || request?.method == HttpMethod.connect) {\n      await clientChannel.write(channelContext, msg);\n      return;\n    }\n\n    // log.i(\"[${clientChannel.id}] Response $msg\");\n\n    HttpResponse? response = msg;\n    //拦截器\n    for (var interceptor in interceptors) {\n      response = await interceptor.onResponse(request!, response!);\n      if (response == null) {\n        logger.d(\"[${clientChannel.id}] Interceptor returned null, stopping processing\");\n        // Interceptor returned null, stopping processing\n        listener?.onResponse(channelContext, msg);\n        channel.close();\n        return;\n      }\n    }\n\n    // Ensure request is linked if not present\n    response?.request ??= request;\n    listener?.onResponse(channelContext, response!);\n    //发送给客户端\n    await clientChannel.write(channelContext, response!);\n  }\n\n  @override\n  void channelInactive(ChannelContext channelContext, Channel channel) {\n    clientChannel.close();\n  }\n\n  @override\n  void exceptionCaught(ChannelContext channelContext, Channel channel, error, {StackTrace? trace}) {\n    super.exceptionCaught(channelContext, channel, error, trace: trace);\n    for (var interceptor in interceptors) {\n      interceptor.onError(channelContext.currentRequest, error, trace);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/network/handle/relay_handle.dart",
    "content": "import 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\n\nclass RelayHandler extends ChannelHandler<Object> {\n  final Channel remoteChannel;\n\n  RelayHandler(this.remoteChannel);\n\n  @override\n  Future<void> channelRead(ChannelContext channelContext, Channel channel, Object msg) async {\n    //发送给客户端\n    remoteChannel.write(channelContext, msg);\n  }\n\n  @override\n  void channelInactive(ChannelContext channelContext, Channel channel) {\n    remoteChannel.close();\n  }\n}\n"
  },
  {
    "path": "lib/network/handle/sse_handle.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/sse.dart';\nimport 'package:proxypin/network/http/websocket.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\n/// SSE (text/event-stream) handler: forwards raw bytes and emits parsed message frames.\nclass SseChannelHandler extends ChannelHandler<Uint8List> {\n  final SseDecoder decoder = SseDecoder();\n\n  final Channel proxyChannel;\n  final HttpMessage message; // HttpResponse on server->client, HttpRequest on client->server\n\n  SseChannelHandler(this.proxyChannel, this.message);\n\n  @override\n  Future<void> channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async {\n    // Always forward the raw bytes first\n    proxyChannel.writeBytes(msg);\n\n    try {\n      final frames = decoder.feed(msg);\n      for (final WebSocketFrame frame in frames) {\n        frame.isFromClient = message is HttpRequest;\n        message.messages.add(frame);\n        channelContext.listener?.onMessage(channel, message, frame);\n        logger.d(\n            \"[${channelContext.clientChannel?.id}] sse channelRead ${frame.payloadLength} ${frame.payloadDataAsString}\");\n      }\n    } catch (e, stackTrace) {\n      log.e(\"sse decode error\", error: e, stackTrace: stackTrace);\n    }\n  }\n}\n\n"
  },
  {
    "path": "lib/network/handle/websocket_handle.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/websocket.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\n/// websocket处理器\nclass WebSocketChannelHandler extends ChannelHandler<Uint8List> {\n  final WebSocketDecoder decoder = WebSocketDecoder();\n\n  final Channel proxyChannel;\n  final HttpMessage message;\n\n  WebSocketChannelHandler(this.proxyChannel, this.message);\n\n  @override\n  Future<void> channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async {\n    proxyChannel.writeBytes(msg);\n    WebSocketFrame? frame;\n    try {\n      frame = decoder.decode(msg);\n    } catch (e, stackTrace) {\n      log.e(\"websocket decode error\", error: e, stackTrace: stackTrace);\n    }\n    if (frame == null) {\n      return;\n    }\n    frame.isFromClient = message is HttpRequest;\n\n    message.messages.add(frame);\n    channelContext.listener?.onMessage(channel, message, frame);\n    logger.d(\n        \"[${channelContext.clientChannel?.id}] websocket channelRead ${frame.payloadLength} ${frame.fin} ${frame.payloadDataAsString}\");\n  }\n}\n"
  },
  {
    "path": "lib/network/http/codec.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:math';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/parse/body_reader.dart';\nimport 'package:proxypin/network/http/constants.dart';\nimport 'package:proxypin/network/http/h2/h2_codec.dart';\nimport 'package:proxypin/network/http/parse/http_parser.dart';\nimport 'package:proxypin/network/util/byte_buf.dart';\n\nimport 'http.dart';\nimport 'http_headers.dart';\n\nclass ParserException implements Exception {\n  final String message;\n  final String? source;\n\n  ParserException(this.message, [this.source]);\n\n  @override\n  String toString() {\n    return 'ParserException{message: $message source: $source}';\n  }\n}\n\nenum State {\n  readInitial,\n  readHeader,\n  body,\n  done,\n}\n\nclass DecoderResult<T> {\n  bool isDone = true;\n  T? data;\n  bool supportedParse;\n\n  //转发消息\n  List<int>? forward;\n\n  DecoderResult({this.isDone = true, this.supportedParse = true});\n}\n\n/// 解码\nabstract interface class Decoder<T> {\n  /// 解码 如果返回null说明数据不完整\n  DecoderResult<T> decode(ChannelContext channelContext, ByteBuf byteBuf);\n}\n\n/// 编码\nabstract interface class Encoder<T> {\n  List<int> encode(ChannelContext channelContext, T data);\n}\n\n/// 编解码器\nabstract class Codec<D, E> implements Decoder<D>, Encoder<E> {\n  static const int defaultMaxInitialLineLength = 1024000; // 1M\n  static const int maxBodyLength = 4096000; // 4M\n}\n\n/// http编解码\nabstract class HttpCodec<T extends HttpMessage> implements Codec<T, T> {\n  final HttpParse _httpParse = HttpParse();\n  Http2Codec<T>? _h2Codec;\n  State _state = State.readInitial;\n\n  late DecoderResult<T> result;\n\n  BodyReader? bodyReader;\n\n  T createMessage(List<String> reqLine);\n\n  Http2Codec<T> getH2Codec() {\n    return _h2Codec ??= (this is HttpRequestCodec ? Http2RequestDecoder() : Http2ResponseDecoder()) as Http2Codec<T>;\n  }\n\n  @override\n  DecoderResult<T> decode(ChannelContext channelContext, ByteBuf data) {\n    var protocol = channelContext.clientChannel?.selectedProtocol;\n\n    if (protocol == HttpConstants.h2 || protocol == HttpConstants.h2_14) {\n      return getH2Codec().decode(channelContext, data);\n    }\n\n    //请求行\n    if (_state == State.readInitial) {\n      init();\n      var initialLine = _readInitialLine(data);\n      if (initialLine.isEmpty) {\n        return result;\n      }\n      result.data = createMessage(initialLine);\n      _state = State.readHeader;\n    }\n\n    //请求头\n    try {\n      if (_state == State.readHeader) {\n        _readHeader(data, result.data!);\n      }\n\n      //请求体\n      if (_state == State.body) {\n        bool resolveBody = channelContext.currentRequest?.method != HttpMethod.head;\n        var bodyResult = resolveBody ? bodyReader!.readBody(data.readAvailableBytes()) : null;\n        if (!resolveBody || bodyResult?.isDone == true) {\n          _state = State.done;\n          result.data!.body = bodyResult?.body;\n        }\n\n        //If the body does not support parsing, forward directly\n        if (bodyResult != null && !bodyResult.supportedParse) {\n          result.supportedParse = false;\n          result.forward = bodyResult.body;\n          return result;\n        }\n      }\n\n      if (_state == State.done) {\n        result.data!.body = _convertBody(result.data!.body);\n        _state = State.readInitial;\n        result.isDone = true;\n        return result;\n      }\n    } catch (e) {\n      _state = State.readInitial;\n      rethrow;\n    }\n\n    return result;\n  }\n\n  void init() {\n    bodyReader = null;\n    result = DecoderResult(isDone: false);\n  }\n\n  void initialLine(BytesBuilder buffer, T message);\n\n  @override\n  List<int> encode(ChannelContext channelContext, T message) {\n    if (message.protocolVersion == \"HTTP/2\") {\n      return getH2Codec().encode(channelContext, message);\n    }\n\n    BytesBuilder builder = BytesBuilder();\n    //请求行\n    initialLine(builder, message);\n\n    List<int>? body = message.body;\n\n    //请求头\n    bool isChunked = message.headers.isChunked;\n    message.headers.remove(HttpHeaders.TRANSFER_ENCODING);\n\n    if (body != null && (body.isNotEmpty || isChunked)) {\n      message.headers.contentLength = body.length;\n    } else if (message.contentLength != 0) {\n      message.headers.remove(HttpHeaders.CONTENT_LENGTH);\n    }\n\n    message.headers.forEach((key, values) {\n      for (var v in values) {\n        builder\n          ..add(key.codeUnits)\n          ..addByte(HttpConstants.colon)\n          ..addByte(HttpConstants.sp)\n          ..add(v.codeUnits)\n          ..addByte(HttpConstants.cr)\n          ..addByte(HttpConstants.lf);\n      }\n    });\n    builder.addByte(HttpConstants.cr);\n    builder.addByte(HttpConstants.lf);\n\n    //请求体\n    builder.add(body ?? Uint8List(0));\n    return builder.toBytes();\n  }\n\n  //读取起始行\n  List<String> _readInitialLine(ByteBuf data) {\n    int maxSize = min(data.readableBytes(), Codec.defaultMaxInitialLineLength);\n    return _httpParse.parseInitialLine(data, maxSize);\n  }\n\n  //读取请求头\n  void _readHeader(ByteBuf data, T message) {\n    if (_httpParse.parseHeaders(data, message.headers)) {\n      _state = State.body;\n      bodyReader = BodyReader(message);\n    }\n  }\n\n  //转换body\n  List<int>? _convertBody(List<int>? bytes) {\n    if (bytes == null) {\n      return null;\n    }\n    return bytes;\n  }\n}\n\n/// http请求编解码\nclass HttpRequestCodec extends HttpCodec<HttpRequest> {\n  @override\n  HttpRequest createMessage(List<String> reqLine) {\n    HttpMethod httpMethod = HttpMethod.valueOf(reqLine[0]);\n    return HttpRequest(httpMethod, reqLine[1], protocolVersion: reqLine[2]);\n  }\n\n  @override\n  void initialLine(BytesBuilder buffer, HttpRequest message) {\n    String uri = message.uri;\n\n    //http scheme 输入地址和host不一致\n    if (uri.startsWith(HostAndPort.httpScheme) &&\n        (message.requestUri?.host != message.headers.host && message.headers.host?.contains(':') != true)) {\n      uri = message.requestUri?.replace(host: message.headers.host).toString() ?? uri;\n    }\n\n    //请求行\n    buffer\n      ..add(message.method.name.codeUnits)\n      ..addByte(HttpConstants.sp)\n      ..add(uri.codeUnits)\n      ..addByte(HttpConstants.sp)\n      ..add(message.protocolVersion.codeUnits)\n      ..addByte(HttpConstants.cr)\n      ..addByte(HttpConstants.lf);\n  }\n}\n\n/// http响应编解码\nclass HttpResponseCodec extends HttpCodec<HttpResponse> {\n  @override\n  HttpResponse createMessage(List<String> reqLine) {\n    var httpStatus = HttpStatus(int.parse(reqLine[1]), reqLine[2]);\n    return HttpResponse(httpStatus, protocolVersion: reqLine[0]);\n  }\n\n  @override\n  void initialLine(BytesBuilder buffer, HttpResponse message) {\n    //状态行\n    buffer.add(message.protocolVersion.codeUnits);\n    buffer.addByte(HttpConstants.sp);\n    buffer.add(message.status.code.toString().codeUnits);\n    buffer.addByte(HttpConstants.sp);\n    buffer.add(message.status.reasonPhrase.codeUnits);\n    buffer.addByte(HttpConstants.cr);\n    buffer.addByte(HttpConstants.lf);\n  }\n}\n\nclass HttpServerCodec extends Codec<HttpRequest, HttpResponse> {\n  HttpRequestCodec requestCodec = HttpRequestCodec();\n  HttpResponseCodec responseCodec = HttpResponseCodec();\n\n  @override\n  DecoderResult<HttpRequest> decode(ChannelContext channelContext, ByteBuf byteBuf) {\n    return requestCodec.decode(channelContext, byteBuf);\n  }\n\n  @override\n  List<int> encode(ChannelContext channelContext, HttpResponse data) {\n    return responseCodec.encode(channelContext, data);\n  }\n}\n\nclass HttpClientCodec extends Codec<HttpResponse, HttpRequest> {\n  HttpRequestCodec requestCodec = HttpRequestCodec();\n  HttpResponseCodec responseCodec = HttpResponseCodec();\n\n  @override\n  DecoderResult<HttpResponse> decode(ChannelContext channelContext, ByteBuf byteBuf) {\n    return responseCodec.decode(channelContext, byteBuf);\n  }\n\n  @override\n  List<int> encode(ChannelContext channelContext, HttpRequest data) {\n    return requestCodec.encode(channelContext, data);\n  }\n}\n"
  },
  {
    "path": "lib/network/http/constants.dart",
    "content": "class HttpConstants {\n  //h2协议\n  static const String h2 = 'h2';\n  static const String h2_14 = 'h2-14';\n\n  /// Line feed character /n\n  static const int lf = 10;\n\n  /// Carriage return /r\n  static const int cr = 13;\n\n  /// Horizontal space\n  static const int sp = 32;\n\n  /// Colon ':'\n  static const int colon = 58;\n\n  /// Colon '='\n  static const int equal = 61;\n}\n"
  },
  {
    "path": "lib/network/http/content_type.dart",
    "content": "/*\n * Copyright Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'package:proxypin/network/util/cache.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\n///content type\n///@author WangHongEn\nenum ContentType {\n  json,\n  formUrl,\n  formData,\n  js,\n  html,\n  text,\n  css,\n  font,\n  image,\n  video,\n  http,\n  sse\n\n  ;\n\n  static ContentType valueOf(String name) {\n    return ContentType.values.firstWhere((element) => element.name == name.toLowerCase(), orElse: () => http);\n  }\n\n  //是否是二进制\n  bool get isBinary {\n    return this == image || this == font || this == video;\n  }\n\n  bool get isImage => this == image;\n}\n\nclass MediaType {\n  static const String wildcardType = \"*/*\";\n  static LruCache<String, MediaType> cachedMediaTypes = LruCache(64);\n\n  ///默认编码类型\n  static List<MediaType> defaultCharsetMediaTypes = [\n    MediaType(\"text\", \"plain\", charset: \"utf-8\"),\n    MediaType(\"text\", \"html\", charset: \"utf-8\"),\n    MediaType(\"text\", \"javascript\", charset: \"utf-8\"),\n    MediaType(\"text\", \"css\", charset: \"utf-8\"),\n    MediaType(\"application\", \"json\", charset: \"utf-8\"),\n    MediaType(\"application\", \"problem+json\", charset: \"utf-8\"),\n    MediaType(\"application\", \"xml\", charset: \"utf-8\"),\n    MediaType(\"application\", \"xhtml+xml\", charset: \"utf-8\"),\n    MediaType(\"application\", \"octet-stream\", charset: \"utf-16\"),\n    MediaType(\"image\", \"*\", charset: \"utf-16\"),\n  ];\n\n  final String type;\n  final String subtype;\n  final Map<String, String> parameters;\n\n  MediaType(this.type, this.subtype, {Map<String, String>? parameters, String? charset})\n      : parameters = parameters ?? {} {\n    if (charset != null) {\n      this.parameters[\"charset\"] = charset;\n    }\n  }\n\n  static MediaType? valueOf(String mediaType) {\n    if (mediaType.isEmpty) {\n      throw InvalidMediaTypeException(mediaType, \"'mediaType' must not be empty\");\n    }\n    // do not cache multipart mime types with random boundaries\n    if (mediaType.startsWith(\"multipart\")) {\n      return _parseMediaTypeInternal(mediaType);\n    }\n\n    var parseMediaType = _parseMediaTypeInternal(mediaType);\n    if (parseMediaType == null) {\n      return null;\n    }\n\n    cachedMediaTypes.set(mediaType, parseMediaType);\n    return parseMediaType;\n  }\n\n  ///编码\n  String? get charset {\n    return parameters[\"charset\"]?.toLowerCase();\n  }\n\n  ///获取默认编码\n  static String? defaultCharset(MediaType mediaType) {\n    for (var defaultMediaType in defaultCharsetMediaTypes) {\n      if (defaultMediaType.equalsTypeAndSubtype(mediaType)) {\n        return defaultMediaType.charset;\n      }\n    }\n    return null;\n  }\n\n  static MediaType? _parseMediaTypeInternal(String mediaType) {\n    int index = mediaType.indexOf(';');\n    String fullType = (index >= 0 ? mediaType.substring(0, index) : mediaType).trim();\n    if (fullType.isEmpty) {\n      logger.d(\"Invalid media type: '$mediaType'\");\n      return null;\n    }\n\n    if (MediaType.wildcardType == fullType) {\n      fullType = \"*/*\";\n    }\n    int subIndex = fullType.indexOf('/');\n    if (subIndex == -1) {\n      logger.d(\"Invalid media type: '$mediaType'\");\n      return null;\n    }\n\n    if (subIndex == fullType.length - 1) {\n      logger.d(\"Invalid media type: '$mediaType'\");\n      return null;\n    }\n\n    String type = fullType.substring(0, subIndex);\n    String subtype = fullType.substring(subIndex + 1);\n    if (MediaType.wildcardType == type && MediaType.wildcardType != subtype) {\n      logger.d(\"Invalid media type: '$mediaType'\");\n      return null;\n    }\n\n    Map<String, String> parameters = {};\n    do {\n      int nextIndex = index + 1;\n      bool quoted = false;\n      while (nextIndex < mediaType.length) {\n        var ch = mediaType[nextIndex];\n        if (ch == ';') {\n          if (!quoted) {\n            break;\n          }\n        } else if (ch == '\"') {\n          quoted = !quoted;\n        }\n        nextIndex++;\n      }\n\n      String parameter = mediaType.substring(index + 1, nextIndex).trim();\n      if (parameter.isNotEmpty) {\n        int eqIndex = parameter.indexOf('=');\n        if (eqIndex >= 0) {\n          String attribute = parameter.substring(0, eqIndex).trim();\n          String value = parameter.substring(eqIndex + 1).trim();\n          parameters[attribute] = value;\n        }\n      }\n      index = nextIndex;\n    } while (index < mediaType.length);\n\n    try {\n      return MediaType(type, subtype, parameters: parameters);\n    } catch (e) {\n      logger.d(\"Invalid media type: '$mediaType'\", error: e);\n      return null;\n    }\n  }\n\n  ///类似于equals（Object），但仅基于类型和子类型，即忽略参数。\n  bool equalsTypeAndSubtype(MediaType other) {\n    return type.toLowerCase() == other.type.toLowerCase() && subtype.toLowerCase() == other.subtype.toLowerCase();\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) {\n      return true;\n    }\n    if (other is MediaType) {\n      return type == other.type && subtype == other.subtype && parameters == other.parameters;\n    }\n    return false;\n  }\n\n  @override\n  int get hashCode => type.hashCode ^ subtype.hashCode ^ parameters.hashCode;\n}\n\nclass InvalidMediaTypeException implements Exception {\n  final String mediaType;\n  final String message;\n\n  InvalidMediaTypeException(this.mediaType, this.message);\n\n  @override\n  String toString() {\n    return \"InvalidMediaTypeException: $message (mediaType: $mediaType)\";\n  }\n}\n"
  },
  {
    "path": "lib/network/http/h2/frame.dart",
    "content": "/*\n * Copyright 2023 the original author or 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\nenum FrameType { data, headers, priority, rstStream, settings, pushPromise, ping, goaway, windowUpdate, continuation }\n\nclass FrameHeader {\n  static const flagsEndStream = 0x01;\n  static const flagsEndHeaders = 0x04;\n  static const flagsPriority = 0x20;\n\n  final int length;\n  final FrameType type;\n  int flags; // 8 bits\n  final int streamIdentifier;\n\n  FrameHeader(this.length, this.type, this.flags, this.streamIdentifier);\n\n  bool get hasPaddedFlag => (flags & 0x08) == 0x08;\n\n  bool get hasPriorityFlag => (flags & flagsPriority) == flagsPriority;\n\n  bool get hasEndHeadersFlag => (flags & flagsEndHeaders) == flagsEndHeaders;\n\n  bool get hasEndStreamFlag => (flags & flagsEndStream) == flagsEndStream;\n\n  bool get hasAckFlag => (flags & 0x01) == 0x01;\n\n  List<int> encode() {\n    var result = <int>[];\n    result.addAll(_intToBytes(length, 3)); // length is 24 bits\n    result.add(type.index); // type is 8 bits\n    result.add(flags); // flags is 8 bits\n    result.addAll(_intToBytes(streamIdentifier, 4)); // streamIdentifier is 32 bits\n    return result;\n  }\n\n  List<int> _intToBytes(int value, int byteCount) {\n    var bytes = <int>[];\n    for (var i = 0; i < byteCount; i++) {\n      bytes.insert(0, value & 0xff);\n      value >>= 8;\n    }\n    return bytes;\n  }\n}\n\nclass Frame {\n  final FrameHeader header;\n\n  Frame(this.header);\n\n  Map toJson() => {\n        'length': header.length,\n        'type': header.type.toString().split('.')[1],\n        'flags': header.flags,\n        'streamIdentifier': header.streamIdentifier\n      };\n}\n\nclass HeadersFrame extends Frame {\n  final int padLength;\n  final bool exclusiveDependency;\n  final int? streamDependency;\n  final int? weight;\n  List<int> headerBlockFragment;\n\n  HeadersFrame(super.header, this.padLength, this.exclusiveDependency, this.streamDependency, this.weight,\n      this.headerBlockFragment);\n\n  @override\n  String toString() {\n    return \"HeadersFrame{padLength: $padLength, exclusiveDependency: $exclusiveDependency, streamDependency: $streamDependency, weight: $weight, headerBlockFragment: ${headerBlockFragment.length}}\";\n  }\n}\n\nclass DataFrame extends Frame {\n  final int padLength;\n  final List<int> data;\n\n  DataFrame(super.header, this.padLength, this.data);\n}\n"
  },
  {
    "path": "lib/network/http/h2/h2_codec.dart",
    "content": "/*\n * Copyright 2023 the original author or 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\nimport 'dart:math';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/codec.dart';\nimport 'package:proxypin/network/http/h2/setting.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\nimport 'package:proxypin/network/util/byte_buf.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/http/sse.dart';\nimport 'package:proxypin/network/http/websocket.dart';\n\nimport '../../util/byte_utils.dart';\nimport 'frame.dart';\nimport 'hpack/hpack.dart';\n\n/// http编解码\nabstract class Http2Codec<T extends HttpMessage> implements Codec<T, T> {\n  static const maxFrameSize = 16384;\n\n  static final List<int> connectionPrefacePRI = \"PRI * HTTP/2.0\\r\\n\\r\\nSM\\r\\n\\r\\n\".codeUnits;\n\n  HPackDecoder decoder = HPackDecoder();\n\n  final HPackEncoder _hpackEncoder = HPackEncoder();\n\n  T createMessage(ChannelContext channelContext, FrameHeader frameHeader, Map<String, List<String>> headers);\n\n  T? getMessage(ChannelContext channelContext, FrameHeader frameHeader);\n\n  // Per-stream SSE decoder instances keyed by HTTP/2 stream id\n  final Map<int, SseDecoder> sseDecoders = {};\n\n  @override\n  DecoderResult<T> decode(ChannelContext channelContext, ByteBuf byteBuf, {bool resolveBody = true}) {\n    DecoderResult<T> result = DecoderResult<T>();\n\n    //Connection Preface PRI * HTTP/2.0\n    if (byteBuf.get(byteBuf.readerIndex) == 0x50 &&\n        byteBuf.get(byteBuf.readerIndex + 1) == 0x52 &&\n        byteBuf.get(byteBuf.readerIndex + 2) == 0x49 &&\n        isConnectionPrefacePRI(byteBuf)) {\n      result.forward = byteBuf.readBytes(connectionPrefacePRI.length);\n      // logger.d(\n      //     \"Connection Preface ${connectionPrefacePRI.length} ${String.fromCharCodes(result.forward!)} ${byteBuf.readableBytes()}\");\n      if (byteBuf.readableBytes() <= 0) {\n        return result;\n      }\n    }\n\n    List<int>? forward = result.forward == null ? null : List.of(result.forward!);\n\n    while (byteBuf.isReadable()) {\n      FrameHeader? frameHeader = FrameReader.readFrameHeader(byteBuf);\n      // logger.d(\n      //     \"frameHeader streamId: ${frameHeader?.streamIdentifier} frame ${frameHeader?.type.name} ${frameHeader?.length} ${byteBuf.readableBytes()}\");\n      if (frameHeader == null) {\n        result.forward = forward;\n        result.isDone = false;\n        return result;\n      }\n\n      List<int>? framePayload = FrameReader._readFramePayload(byteBuf, frameHeader.length);\n      if (framePayload == null) {\n        result.isDone = false;\n        byteBuf.readerIndex -= FrameReader.headerLength;\n\n        result.forward = forward;\n        return result;\n      }\n\n      var parseResult = parseHttp2Packet(channelContext, frameHeader, framePayload);\n      if (parseResult.forward != null) {\n        forward ??= [];\n        forward.addAll(parseResult.forward!);\n      }\n\n      if (parseResult.isDone) {\n        parseResult.forward = forward;\n        return parseResult;\n      }\n    }\n\n    result.forward = forward;\n    result.isDone = false;\n    return result;\n  }\n\n  DecoderResult<T> parseHttp2Packet(ChannelContext channelContext, FrameHeader frameHeader, List<int> framePayload) {\n    var result = DecoderResult<T>(isDone: false);\n\n    // logger.d(\n    //     \"[${channelContext.clientChannel?.id}] ${this is Http2RequestDecoder ? 'request' : 'response'} streamId:${frameHeader.streamIdentifier} ${frameHeader.type} endHeaders: ${frameHeader.hasEndHeadersFlag} \"\n    //     \"endStream: ${frameHeader.hasEndStreamFlag} ${frameHeader.length}\");\n    //根据帧类型进行处理\n    switch (frameHeader.type) {\n      case FrameType.headers:\n        //处理HEADERS帧\n        var headersFrame = _handleHeadersFrame(channelContext, frameHeader, ByteBuf(framePayload));\n        result.isDone = frameHeader.hasEndStreamFlag && frameHeader.hasEndHeadersFlag;\n        if (headersFrame.streamDependency != null) {\n          headersFrame.headerBlockFragment = [];\n          channelContext.put(frameHeader.streamIdentifier, headersFrame);\n        }\n\n        //handle special case for SSE\n        var possibleMessage = getMessage(channelContext, frameHeader);\n        if (possibleMessage is HttpResponse &&\n            possibleMessage.headers.contentType.toLowerCase().startsWith('text/event-stream')) {\n          result.forward = List.from(frameHeader.encode())..addAll(framePayload);\n          result.data = possibleMessage;\n          var currentRequest = channelContext.getStreamRequest(frameHeader.streamIdentifier);\n          currentRequest?.response = possibleMessage;\n          possibleMessage.request ??= channelContext.currentRequest;\n          channelContext.listener?.onResponse(channelContext, possibleMessage);\n          return result;\n        }\n\n        break;\n      case FrameType.continuation:\n        //处理CONTINUATION帧\n        var message = getMessage(channelContext, frameHeader);\n        if (message == null) {\n          logger.e(\"CONTINUATION frame but no message found\");\n          result.forward = List.from(frameHeader.encode())..addAll(framePayload);\n          return result;\n        }\n\n        Map<String, List<String>> headers = _parseHeaders(channelContext, framePayload);\n        headers.forEach((key, values) => message.headers.addValues(key, values));\n        message.packageSize = (message.packageSize ?? 0) + frameHeader.length;\n        if (frameHeader.hasEndHeadersFlag &&\n            channelContext.getStreamRequest(frameHeader.streamIdentifier)?.method == HttpMethod.head) {\n          result.isDone = true;\n        }\n\n        break;\n      case FrameType.data:\n        //处理DATA帧\n        var message = getMessage(channelContext, frameHeader)!;\n        bool isSseResponse =\n            message is HttpResponse && message.headers.contentType.toLowerCase().startsWith('text/event-stream');\n        if (isSseResponse) {\n          _handleSseDataFrame(channelContext, frameHeader, message, ByteBuf(framePayload));\n          result.forward = List.from(frameHeader.encode())..addAll(framePayload);\n          return result;\n        }\n\n        _handleDataFrame(channelContext, frameHeader, message, ByteBuf(framePayload));\n        result.isDone = frameHeader.hasEndStreamFlag;\n        break;\n      case FrameType.settings:\n        SettingHandler.handleSettingsFrame(channelContext, frameHeader, ByteBuf(framePayload));\n        result.forward = List.from(frameHeader.encode())..addAll(framePayload);\n        return result;\n      case FrameType.goaway:\n        var lastStreamId = readInt32(framePayload, 0);\n        var errorCode = readInt32(framePayload, 4);\n        var debugData = viewOrSublist(framePayload, 8, frameHeader.length - 8);\n        logger.i(\n            \"[${channelContext.clientChannel?.id}] ${this is Http2RequestDecoder ? 'request' : 'response'} h2 goaway streamId: ${frameHeader.streamIdentifier} lastStreamId: $lastStreamId errorCode: $errorCode debugData: ${String.fromCharCodes(debugData)}\");\n        result.forward = List.from(frameHeader.encode())..addAll(framePayload);\n        return result;\n      default:\n        //其他帧类型 原文转发\n        result.forward = List.from(frameHeader.encode())..addAll(framePayload);\n        return result;\n    }\n\n    if (result.isDone && frameHeader.streamIdentifier > 0) {\n      result.data = getMessage(channelContext, frameHeader);\n      result.data?.streamId = frameHeader.streamIdentifier;\n      channelContext.currentRequest = channelContext.getStreamRequest(frameHeader.streamIdentifier);\n\n      if (result.data is HttpResponse) {\n        channelContext.removeStream(frameHeader.streamIdentifier);\n      }\n    }\n\n    return result;\n  }\n\n  List<Header> encodeHeaders(T message);\n\n  @override\n  Uint8List encode(ChannelContext channelContext, T data) {\n    var bytesBuilder = BytesBuilder();\n    if (data.headers.getInt(HttpHeaders.CONTENT_LENGTH) != null) {\n      data.headers.set(HttpHeaders.CONTENT_LENGTH.toLowerCase(), \"${data.body?.length ?? 0}\");\n    }\n\n    var emptyBody = data.body == null || data.body!.isEmpty;\n\n    //headers\n    var headers = encodeHeaders(data);\n\n    writeHeadersFrame(bytesBuilder, channelContext, data.streamId!, headers, endStream: emptyBody);\n\n    //body\n    if (!emptyBody) {\n      var payload = data.body!;\n      while (payload.length > maxFrameSize) {\n        var chunkSize = min(maxFrameSize, payload.length);\n        var chunk = payload.sublist(0, chunkSize);\n        payload = payload.sublist(chunkSize);\n        _writeFrame(channelContext, bytesBuilder, FrameType.data, 0, data.streamId!, chunk);\n      }\n\n      _writeFrame(channelContext, bytesBuilder, FrameType.data, FrameHeader.flagsEndStream, data.streamId!, payload);\n    }\n\n    return bytesBuilder.takeBytes();\n  }\n\n  void writeHeadersFrame(\n    BytesBuilder bytesBuilder,\n    ChannelContext channelContext,\n    int streamId,\n    List<Header> headers, {\n    StreamSetting? setting,\n    bool endStream = true,\n  }) {\n    var fragment = _hpackEncoder.encode(headers);\n    var maxSize = channelContext.setting?.maxFrameSize ?? maxFrameSize;\n\n    if (fragment.length < maxSize) {\n      int flags = FrameHeader.flagsEndHeaders;\n      if (endStream) {\n        flags |= FrameHeader.flagsEndStream;\n      }\n      _writeHeadersFrame(bytesBuilder, channelContext, flags, streamId, fragment);\n    } else {\n      var chunk = fragment.sublist(0, maxSize);\n      fragment = fragment.sublist(maxSize);\n\n      _writeHeadersFrame(bytesBuilder, channelContext, 0, streamId, chunk);\n\n      while (fragment.length > maxSize) {\n        var chunk = fragment.sublist(0, maxSize);\n        fragment = fragment.sublist(maxSize);\n        _writeFrame(channelContext, bytesBuilder, FrameType.continuation, 0, streamId, chunk);\n      }\n\n      _writeFrame(\n          channelContext, bytesBuilder, FrameType.continuation, FrameHeader.flagsEndHeaders, streamId, fragment);\n\n      if (endStream) {\n        //如果没有body，发送一个空的DATA帧\n        _writeFrame(channelContext, bytesBuilder, FrameType.data, FrameHeader.flagsEndStream, streamId, []);\n      }\n    }\n  }\n\n  void _writeHeadersFrame(\n      BytesBuilder bytesBuilder, ChannelContext channelContext, int flags, int streamId, List<int> payload) {\n    var streamPriority = channelContext.removeStreamDependency(streamId);\n    if (streamPriority != null) {\n      flags |= FrameHeader.flagsPriority;\n      bool exclusive = streamPriority.exclusiveDependency;\n      int streamDependency = streamPriority.streamDependency!;\n\n      payload = [\n        (exclusive ? 0x80 : 0) | (streamDependency & 0x7FFFFFFF) >> 24,\n        (streamDependency & 0x00FF0000) >> 16,\n        (streamDependency & 0x0000FF00) >> 8,\n        (streamDependency & 0x000000FF),\n        streamPriority.weight!,\n        ...payload\n      ];\n    }\n\n    // logger.d(\n    //     \"[${channelContext.clientChannel?.id}] ${this is Http2RequestDecoder ? 'request' : 'response'} _writeHeadersFrame streamId:$streamId  flags:$flags originFlags:${streamPriority?.header.flags} ${streamPriority} ${payload.length}\");\n\n    _writeFrame(channelContext, bytesBuilder, FrameType.headers, flags, streamId, payload);\n  }\n\n  void _writeFrame(ChannelContext channelContext, BytesBuilder bytesBuilder, FrameType type, int flags, int streamId,\n      List<int> payload) {\n    FrameHeader frameHeader = FrameHeader(payload.length, type, flags, streamId);\n    // logger.d(\n    //     \"[${channelContext.clientChannel?.id}] ${this is Http2RequestDecoder ? 'request' : 'response'} _writeFrame streamId:${frameHeader.streamIdentifier}  ${frameHeader.type} flags:${frameHeader.flags} endHeaders: ${frameHeader.hasEndHeadersFlag} endStream: ${frameHeader.hasEndStreamFlag} ${payload.length}\");\n\n    bytesBuilder.add(frameHeader.encode());\n    bytesBuilder.add(payload);\n  }\n\n  bool isConnectionPrefacePRI(ByteBuf data) {\n    if (data.readableBytes() < 9) {\n      return false;\n    }\n    for (int i = 0; i < connectionPrefacePRI.length; i++) {\n      if (data.get(data.readerIndex + i) != connectionPrefacePRI[i]) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  void _handleSseDataFrame(\n      ChannelContext channelContext, FrameHeader frameHeader, HttpMessage message, ByteBuf payload) {\n    //  DATA 帧格式\n    int padLength = 0;\n    if (frameHeader.hasPaddedFlag) {\n      padLength = payload.readByte();\n    }\n    int dataLength = payload.readableBytes() - padLength;\n    var data = payload.readBytes(dataLength);\n    // Incremental SSE parsing: do not accumulate full body to avoid large memory usage\n    final decoder = sseDecoders.putIfAbsent(frameHeader.streamIdentifier, () => SseDecoder());\n    final frames = decoder.feed(Uint8List.fromList(data));\n    for (final WebSocketFrame frame in frames) {\n      frame.isFromClient = false; // server -> client\n      message.messages.add(frame);\n      channelContext.listener?.onMessage(channelContext.clientChannel!, message, frame);\n      logger.d(\n          '[${channelContext.clientChannel?.id}] h2 sse streamId:${frameHeader.streamIdentifier} frame ${frame.payloadLength} ${frame.payloadDataAsString}');\n    }\n\n    if (frameHeader.hasEndStreamFlag) {\n      sseDecoders.remove(frameHeader.streamIdentifier);\n      channelContext.removeStream(frameHeader.streamIdentifier);\n    }\n  }\n\n  DataFrame _handleDataFrame(\n      ChannelContext channelContext, FrameHeader frameHeader, HttpMessage message, ByteBuf payload) {\n    //  DATA 帧格式\n    int padLength = 0;\n    if (frameHeader.hasPaddedFlag) {\n      padLength = payload.readByte();\n    }\n\n    //读取数据\n    int dataLength = payload.readableBytes() - padLength;\n    var data = payload.readBytes(dataLength);\n\n    // Regular body accumulation\n    if (message.body == null) {\n      message.body = data;\n    } else {\n      message.body = List.from(message.body!)..addAll(data);\n    }\n    message.packageSize = (message.packageSize ?? 0) + frameHeader.length;\n    return DataFrame(frameHeader, padLength, data);\n  }\n\n  HeadersFrame _handleHeadersFrame(ChannelContext channelContext, FrameHeader frameHeader, ByteBuf payload) {\n    //  HEADERS 帧格式\n    int padLength = 0;\n    //如果帧头部有PADDED标志位，则需要读取PADDED长度\n    if (frameHeader.hasPaddedFlag) {\n      padLength = payload.readByte();\n    }\n\n    int? streamDependency;\n    bool exclusiveDependency = false;\n    int? weight;\n    //如果帧头部有PRIORITY标志位，则需要读取优先级信息\n    if (frameHeader.hasPriorityFlag) {\n      if (payload.readableBytes() < 5) {\n        throw Exception(\"Invalid PRIORITY frame: insufficient data\");\n      }\n\n      // 读取依赖流 ID 和权重\n      int dependency = payload.readInt();\n      exclusiveDependency = (dependency & 0x80000000) != 0; // 检查最高位是否为 1\n      streamDependency = dependency & 0x7FFFFFFF; // 获取低 31 位\n      weight = payload.readByte(); // 读取权重\n\n      logger.d(\n          \"PRIORITY frame parsed: streamId:${frameHeader.streamIdentifier} streamDependency=$streamDependency, weight=$weight $exclusiveDependency\");\n    }\n\n    var headerBlockLength = payload.length - payload.readerIndex - padLength;\n    if (headerBlockLength < 0) {\n      throw Exception(\"headerBlockLength < 0\");\n    }\n\n    var blockFragment = payload.readBytes(headerBlockLength);\n\n    //读取头部信息\n    Map<String, List<String>> headers = _parseHeaders(channelContext, blockFragment);\n\n    T message = createMessage(channelContext, frameHeader, headers);\n\n    headers.forEach((key, values) {\n      if (!key.startsWith(\":\")) {\n        message.headers.addValues(key, values);\n      }\n    });\n\n    message.streamId = frameHeader.streamIdentifier;\n    message.packageSize = frameHeader.length;\n    return HeadersFrame(frameHeader, padLength, exclusiveDependency, streamDependency, weight, blockFragment);\n  }\n\n  Map<String, List<String>> _parseHeaders(ChannelContext channelContext, List<int> payload) {\n    if (channelContext.setting != null) {\n      decoder.updateMaxReceivingHeaderTableSize(channelContext.setting!.headTableSize);\n    }\n\n    // Decode the headers\n    List<Header> headers = decoder.decode(payload);\n\n    // Convert the headers to a map\n    Map<String, List<String>> headerMap = {};\n    for (Header header in headers) {\n      final name = header.nameString;\n      final value = header.valueString;\n      headerMap[name] ??= [];\n      headerMap[name]!.add(value);\n    }\n\n    return headerMap;\n  }\n}\n\nclass Http2RequestDecoder extends Http2Codec<HttpRequest> {\n  @override\n  HttpRequest createMessage(ChannelContext channelContext, FrameHeader frameHeader, Map<String, List<String>> headers) {\n    HttpMethod httpMethod = HttpMethod.valueOf(headers[\":method\"]!.first);\n\n    var httpRequest =\n        HttpRequest(httpMethod, headers[\":path\"]!.first, protocolVersion: headers[\":version\"]?.firstOrNull ?? \"HTTP/2\");\n\n    String? authority = headers[\":authority\"]?.firstOrNull;\n    String? scheme = headers[\":scheme\"]?.firstOrNull;\n\n    if (authority == null || scheme == null) {\n      logger.e(\"Invalid HTTP/2 request headers: $headers\");\n    } else {\n      // 解析 authority，提取主机和端口\n      String host = authority;\n      int port = (scheme == 'https' ? 443 : 80);\n\n      if (authority.startsWith(\"[\")) {\n        int closeBracketIndex = authority.indexOf(']');\n        if (closeBracketIndex != -1) {\n          host = authority.substring(0, closeBracketIndex + 1);\n          if (authority.length > closeBracketIndex + 1 && authority[closeBracketIndex + 1] == ':') {\n            port = int.tryParse(authority.substring(closeBracketIndex + 2)) ?? port;\n          }\n        }\n      } else {\n        int lastColonIndex = authority.lastIndexOf(':');\n        if (lastColonIndex != -1) {\n          var p = int.tryParse(authority.substring(lastColonIndex + 1));\n          if (p != null) {\n            host = authority.substring(0, lastColonIndex);\n            port = p;\n          }\n        }\n      }\n      httpRequest.hostAndPort = HostAndPort(\"$scheme://\", host, port);\n    }\n\n    var old = channelContext.putStreamRequest(frameHeader.streamIdentifier, httpRequest);\n    assert(old == null, \"old request is not null\");\n    return httpRequest;\n  }\n\n  @override\n  HttpRequest? getMessage(ChannelContext channelContext, FrameHeader frameHeader) {\n    return channelContext.getStreamRequest(frameHeader.streamIdentifier);\n  }\n\n  @override\n  List<Header> encodeHeaders(HttpRequest message) {\n    var headers = <Header>[];\n    var uri = message.requestUri!;\n    headers.add(Header.ascii(\":method\", message.method.name));\n    headers.add(Header.ascii(\":scheme\", uri.scheme));\n    headers.add(Header.ascii(\":authority\", uri.host));\n    headers.add(Header.ascii(\":path\", message.uri));\n\n    message.headers.forEach((key, values) {\n      for (var value in values) {\n        headers.add(Header.ascii(key.toLowerCase(), value));\n      }\n    });\n    return headers;\n  }\n}\n\nclass Http2ResponseDecoder extends Http2Codec<HttpResponse> {\n  @override\n  HttpResponse createMessage(\n      ChannelContext channelContext, FrameHeader frameHeader, Map<String, List<String>> headers) {\n    var httpResponse = HttpResponse(HttpStatus.valueOf(int.parse(headers[':status']!.first)),\n        protocolVersion: headers[\":version\"]?.firstOrNull ?? 'HTTP/2');\n    final requestId = channelContext.getStreamRequest(frameHeader.streamIdentifier)?.requestId;\n    if (requestId != null) {\n      httpResponse.requestId = requestId;\n    }\n    channelContext.putStreamResponse(frameHeader.streamIdentifier, httpResponse);\n    return httpResponse;\n  }\n\n  @override\n  HttpResponse? getMessage(ChannelContext channelContext, FrameHeader frameHeader) {\n    return channelContext.getStreamResponse(frameHeader.streamIdentifier);\n  }\n\n  @override\n  List<Header> encodeHeaders(HttpResponse message) {\n    var headers = <Header>[];\n    headers.add(Header.ascii(\":status\", message.status.code.toString()));\n    message.headers.forEach((key, values) {\n      for (var value in values) {\n        headers.add(Header.ascii(key, value));\n      }\n    });\n    return headers;\n  }\n}\n\nclass FrameReader {\n  static int headerLength = 9;\n\n  static List<int>? _readFramePayload(ByteBuf data, int length) {\n    if (data.readableBytes() < length) {\n      return null;\n    }\n\n    var readBytes = data.readBytes(length);\n    data.clearRead();\n    return readBytes;\n  }\n\n  static FrameHeader? readFrameHeader(ByteBuf data) {\n    if (data.readableBytes() < headerLength) {\n      return null;\n    }\n\n    int length = data.read() << 16 | data.read() << 8 | data.read();\n    FrameType type = FrameType.values[data.read()];\n    int flags = data.read();\n    int streamIdentifier = data.readInt();\n\n    return FrameHeader(length, type, flags, streamIdentifier);\n  }\n}\n"
  },
  {
    "path": "lib/network/http/h2/hpack/hpack.dart",
    "content": "// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file\n// for details. All rights reserved. Use of this source code is governed by a\n// BSD-style license that can be found in the LICENSE file.\n\n/// HPACK specification. See here for more information:\n///   https://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10\n\nimport 'dart:convert' show ascii;\nimport 'dart:typed_data';\n\nimport '../../../util/byte_utils.dart';\nimport 'huffman.dart';\nimport 'huffman_table.dart';\n\n/// Exception raised due to encoding/decoding errors.\nclass HPackDecodingException implements Exception {\n  final String _message;\n\n  HPackDecodingException(this._message);\n\n  @override\n  String toString() => 'HPackDecodingException: $_message';\n}\n\n/// A HPACK encoding/decoding context.\n///\n/// This is a statefull class, so encoding/decoding changes internal state.\nclass HPackContext {\n  final HPackEncoder encoder = HPackEncoder();\n  final HPackDecoder decoder = HPackDecoder();\n\n  HPackContext({\n    int maxSendingHeaderTableSize = 4096,\n    int maxReceivingHeaderTableSize = 4096,\n  }) {\n    encoder.updateMaxSendingHeaderTableSize(maxSendingHeaderTableSize);\n    decoder.updateMaxReceivingHeaderTableSize(maxReceivingHeaderTableSize);\n  }\n}\n\n/// A HTTP/2 header.\nclass Header {\n  final List<int> name;\n  final List<int> value;\n  final bool neverIndexed;\n\n  Header(this.name, this.value, {this.neverIndexed = false});\n\n  String get nameString => ascii.decode(name);\n\n  String get valueString => ascii.decode(value);\n\n  factory Header.ascii(String name, String value) {\n    // Specs: `However, header field names MUST be converted to lowercase prior\n    // to their encoding in HTTP/2. A request or response containing uppercase\n    // header field names MUST be treated as malformed (Section 8.1.2.6).`\n    return Header(ascii.encode(name.toLowerCase()), ascii.encode(value));\n  }\n\n}\n\n/// A stateful HPACK decoder.\nclass HPackDecoder {\n  int _maxHeaderTableSize = 4096;\n\n  final IndexTable _table = IndexTable();\n\n  void updateMaxReceivingHeaderTableSize(int newMaximumSize) {\n    _maxHeaderTableSize = newMaximumSize;\n  }\n\n  List<Header> decode(List<int> data) {\n    var offset = 0;\n\n    int readInteger(int prefixBits) {\n      assert(prefixBits <= 8 && prefixBits > 0);\n\n      var byte = data[offset++] & ((1 << prefixBits) - 1);\n\n      int integer;\n      if (byte == ((1 << prefixBits) - 1)) {\n        // Length encodeded.\n        integer = 0;\n        var shift = 0;\n        while (true) {\n          var done = (data[offset] & 0x80) != 0x80;\n          integer += (data[offset++] & 0x7f) << shift;\n          shift += 7;\n          if (done) break;\n        }\n        integer += (1 << prefixBits) - 1;\n      } else {\n        // In place length.\n        integer = byte;\n      }\n\n      return integer;\n    }\n\n    List<int> readStringLiteral() {\n      var isHuffmanEncoding = (data[offset] & 0x80) != 0;\n      var length = readInteger(7);\n\n      var sublist = viewOrSublist(data, offset, length);\n      offset += length;\n      if (isHuffmanEncoding) {\n        return http2HuffmanCodec.decode(sublist);\n      } else {\n        return sublist;\n      }\n    }\n\n    Header readHeaderFieldInternal(int index, {bool neverIndexed = false}) {\n      List<int> name, value;\n      if (index > 0) {\n        name = _table.lookup(index).name;\n        value = readStringLiteral();\n      } else {\n        name = readStringLiteral();\n        value = readStringLiteral();\n      }\n      return Header(name, value, neverIndexed: neverIndexed);\n    }\n\n    try {\n      var headers = <Header>[];\n      while (offset < data.length) {\n        var byte = data[offset];\n        var isIndexedField = (byte & 0x80) != 0;\n        var isIncrementalIndexing = (byte & 0xc0) == 0x40;\n\n        var isWithoutIndexing = (byte & 0xf0) == 0;\n        var isNeverIndexing = (byte & 0xf0) == 0x10;\n        var isDynamicTableSizeUpdate = (byte & 0xe0) == 0x20;\n\n        if (isIndexedField) {\n          var index = readInteger(7);\n          var field = _table.lookup(index);\n          headers.add(field);\n        } else if (isIncrementalIndexing) {\n          var field = readHeaderFieldInternal(readInteger(6));\n          _table.addHeaderField(field);\n          headers.add(field);\n        } else if (isWithoutIndexing) {\n          headers.add(readHeaderFieldInternal(readInteger(4)));\n        } else if (isNeverIndexing) {\n          headers.add(\n            readHeaderFieldInternal(readInteger(4), neverIndexed: true),\n          );\n        } else if (isDynamicTableSizeUpdate) {\n          var newMaxSize = readInteger(5);\n          _table.updateMaxSize(newMaxSize);\n        } else {\n          throw HPackDecodingException('Invalid encoding of headers.');\n        }\n      }\n      return headers;\n      // ignore: avoid_catching_errors\n    } on RangeError catch (e) {\n      throw HPackDecodingException('$e');\n    } on HuffmanDecodingException catch (e) {\n      throw HPackDecodingException('$e');\n    }\n  }\n}\n\n/// A stateful HPACK encoder.\n//  Currently we encode all headers:\n//    - without huffman encoding\n//    - without using the dynamic table\nclass HPackEncoder {\n  void updateMaxSendingHeaderTableSize(int newMaximumSize) {\n    // Once we start encoding via dynamic table we need to let the other\n    // side know the maximum table size we're using.\n  }\n\n  List<int> encode(List<Header> headers) {\n    var bytesBuilder = BytesBuilder();\n    var currentByte = 0;\n\n    void writeInteger(int prefixBits, int value) {\n      assert(prefixBits <= 8);\n\n      if (value < (1 << prefixBits) - 1) {\n        currentByte |= value;\n        bytesBuilder.addByte(currentByte);\n      } else {\n        // Length encodeded.\n        currentByte |= (1 << prefixBits) - 1;\n        value -= (1 << prefixBits) - 1;\n        bytesBuilder.addByte(currentByte);\n        var done = false;\n        while (!done) {\n          currentByte = value & 0x7f;\n          value = value >> 7;\n          done = value == 0;\n          if (!done) currentByte |= 0x80;\n          bytesBuilder.addByte(currentByte);\n        }\n      }\n      currentByte = 0;\n    }\n\n    void writeStringLiteral(List<int> bytes) {\n      // Support huffman encoding.\n      currentByte = 0; // 1 would be huffman encoding\n      writeInteger(7, bytes.length);\n      bytesBuilder.add(bytes);\n    }\n\n    void writeLiteralHeaderWithoutIndexing(Header header) {\n      bytesBuilder.addByte(0);\n      writeStringLiteral(header.name);\n      writeStringLiteral(header.value);\n    }\n\n    for (var header in headers) {\n      writeLiteralHeaderWithoutIndexing(header);\n    }\n\n    return bytesBuilder.takeBytes();\n  }\n}\n\nclass IndexTable {\n  static final List<Header?> _staticTable = [\n    null,\n    Header(ascii.encode(':authority'), const []),\n    Header(ascii.encode(':method'), ascii.encode('GET')),\n    Header(ascii.encode(':method'), ascii.encode('POST')),\n    Header(ascii.encode(':path'), ascii.encode('/')),\n    Header(ascii.encode(':path'), ascii.encode('/index.html')),\n    Header(ascii.encode(':scheme'), ascii.encode('http')),\n    Header(ascii.encode(':scheme'), ascii.encode('https')),\n    Header(ascii.encode(':status'), ascii.encode('200')),\n    Header(ascii.encode(':status'), ascii.encode('204')),\n    Header(ascii.encode(':status'), ascii.encode('206')),\n    Header(ascii.encode(':status'), ascii.encode('304')),\n    Header(ascii.encode(':status'), ascii.encode('400')),\n    Header(ascii.encode(':status'), ascii.encode('404')),\n    Header(ascii.encode(':status'), ascii.encode('500')),\n    Header(ascii.encode('accept-charset'), const []),\n    Header(ascii.encode('accept-encoding'), ascii.encode('gzip, deflate')),\n    Header(ascii.encode('accept-language'), const []),\n    Header(ascii.encode('accept-ranges'), const []),\n    Header(ascii.encode('accept'), const []),\n    Header(ascii.encode('access-control-allow-origin'), const []),\n    Header(ascii.encode('age'), const []),\n    Header(ascii.encode('allow'), const []),\n    Header(ascii.encode('authorization'), const []),\n    Header(ascii.encode('cache-control'), const []),\n    Header(ascii.encode('content-disposition'), const []),\n    Header(ascii.encode('content-encoding'), const []),\n    Header(ascii.encode('content-language'), const []),\n    Header(ascii.encode('content-length'), const []),\n    Header(ascii.encode('content-location'), const []),\n    Header(ascii.encode('content-range'), const []),\n    Header(ascii.encode('content-type'), const []),\n    Header(ascii.encode('cookie'), const []),\n    Header(ascii.encode('date'), const []),\n    Header(ascii.encode('etag'), const []),\n    Header(ascii.encode('expect'), const []),\n    Header(ascii.encode('expires'), const []),\n    Header(ascii.encode('from'), const []),\n    Header(ascii.encode('host'), const []),\n    Header(ascii.encode('if-match'), const []),\n    Header(ascii.encode('if-modified-since'), const []),\n    Header(ascii.encode('if-none-match'), const []),\n    Header(ascii.encode('if-range'), const []),\n    Header(ascii.encode('if-unmodified-since'), const []),\n    Header(ascii.encode('last-modified'), const []),\n    Header(ascii.encode('link'), const []),\n    Header(ascii.encode('location'), const []),\n    Header(ascii.encode('max-forwards'), const []),\n    Header(ascii.encode('proxy-authenticate'), const []),\n    Header(ascii.encode('proxy-authorization'), const []),\n    Header(ascii.encode('range'), const []),\n    Header(ascii.encode('referer'), const []),\n    Header(ascii.encode('refresh'), const []),\n    Header(ascii.encode('retry-after'), const []),\n    Header(ascii.encode('server'), const []),\n    Header(ascii.encode('set-cookie'), const []),\n    Header(ascii.encode('strict-transport-security'), const []),\n    Header(ascii.encode('transfer-encoding'), const []),\n    Header(ascii.encode('user-agent'), const []),\n    Header(ascii.encode('vary'), const []),\n    Header(ascii.encode('via'), const []),\n    Header(ascii.encode('www-authenticate'), const []),\n  ];\n\n  final List<Header> _dynamicTable = [];\n\n  /// The maximum size the dynamic table can grow to before entries need to be\n  /// evicted.\n  int _maximumSize = 4096;\n\n  /// The current size of the dynamic table.\n  int _currentSize = 0;\n\n  IndexTable();\n\n  /// Updates the maximum size which the dynamic table can grow to.\n  void updateMaxSize(int newMaxDynTableSize) {\n    _maximumSize = newMaxDynTableSize;\n    _reduce();\n  }\n\n  /// Lookup an item by index.\n  Header lookup(int index) {\n    if (index <= 0) {\n      throw HPackDecodingException(\n        'Invalid index (was: $index) for table lookup.',\n      );\n    }\n    if (index < _staticTable.length) {\n      return _staticTable[index]!;\n    }\n    index -= _staticTable.length;\n    if (index < _dynamicTable.length) {\n      return _dynamicTable[index];\n    }\n    throw HPackDecodingException(\n      'Invalid index (was: $index) for table lookup.',\n    );\n  }\n\n  /// Adds a new header field to the dynamic table - and evicts entries as\n  /// necessary.\n  void addHeaderField(Header header) {\n    _dynamicTable.insert(0, header);\n    _currentSize += _sizeOf(header);\n    _reduce();\n  }\n\n  /// Removes as many entries as required to be within the limit of\n  /// [_maximumSize].\n  void _reduce() {\n    while (_currentSize > _maximumSize) {\n      var h = _dynamicTable.removeLast();\n      _currentSize -= _sizeOf(h);\n    }\n  }\n\n  /// Returns the \"size\" a [header] has.\n  ///\n  /// This is specified to be the number of octets of name/value plus 32.\n  int _sizeOf(Header header) => header.name.length + header.value.length + 32;\n}\n"
  },
  {
    "path": "lib/network/http/h2/hpack/huffman.dart",
    "content": "// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file\n// for details. All rights reserved. Use of this source code is governed by a\n// BSD-style license that can be found in the LICENSE file.\n\nimport 'dart:typed_data';\n\nimport 'huffman_table.dart';\n\nclass HuffmanDecodingException implements Exception {\n  final String _message;\n\n  HuffmanDecodingException(this._message);\n\n  @override\n  String toString() => 'HuffmanDecodingException: $_message';\n}\n\n/// A codec used for encoding/decoding using a huffman codec.\nclass HuffmanCodec {\n  final HuffmanEncoder _encoder;\n  final HuffmanDecoder _decoder;\n\n  HuffmanCodec(this._encoder, this._decoder);\n\n  List<int> decode(List<int> bytes) => _decoder.decode(bytes);\n\n  List<int> encode(List<int> bytes) => _encoder.encode(bytes);\n}\n\n/// A huffman decoder based on a [HuffmanTreeNode].\nclass HuffmanDecoder {\n  final HuffmanTreeNode _root;\n\n  HuffmanDecoder(this._root);\n\n  /// Decodes [bytes] using a huffman tree.\n  List<int> decode(List<int> bytes) {\n    var buffer = BytesBuilder();\n\n    var currentByteOffset = 0;\n    var node = _root;\n    var currentDepth = 0;\n    while (currentByteOffset < bytes.length) {\n      var byte = bytes[currentByteOffset];\n      for (var currentBit = 7; currentBit >= 0; currentBit--) {\n        var right = (byte >> currentBit) & 1 == 1;\n        if (right) {\n          node = node.right!;\n        } else {\n          node = node.left!;\n        }\n        currentDepth++;\n        if (node.value != null) {\n          if (node.value == EOS_BYTE) {\n            throw HuffmanDecodingException(\n              'More than 7 bit padding is not allowed. Found entire EOS '\n              'encoding',\n            );\n          }\n          buffer.addByte(node.value!);\n          node = _root;\n          currentDepth = 0;\n        }\n      }\n      currentByteOffset++;\n    }\n\n    if (node != _root) {\n      if (currentDepth > 7) {\n        throw HuffmanDecodingException(\n          'Incomplete encoding of a byte or more than 7 bit padding.',\n        );\n      }\n\n      while (node.right != null) {\n        node = node.right!;\n      }\n\n      if (node.value != 256) {\n        throw HuffmanDecodingException('Incomplete encoding of a byte.');\n      }\n    }\n\n    return buffer.takeBytes();\n  }\n}\n\n/// A huffman encoder based on a list of codewords.\nclass HuffmanEncoder {\n  final List<EncodedHuffmanValue> _codewords;\n\n  HuffmanEncoder(this._codewords);\n\n  /// Encodes [bytes] using a list of codewords.\n  List<int> encode(List<int> bytes) {\n    var buffer = BytesBuilder();\n\n    var currentByte = 0;\n    var currentBitOffset = 7;\n\n    void writeValue(int value, int numBits) {\n      var i = numBits - 1;\n      while (i >= 0) {\n        if (currentBitOffset == 7 && i >= 7) {\n          assert(currentByte == 0);\n\n          buffer.addByte((value >> (i - 7)) & 0xff);\n          currentBitOffset = 7;\n          currentByte = 0;\n          i -= 8;\n        } else {\n          currentByte |= ((value >> i) & 1) << currentBitOffset;\n\n          currentBitOffset--;\n          if (currentBitOffset == -1) {\n            buffer.addByte(currentByte);\n            currentBitOffset = 7;\n            currentByte = 0;\n          }\n          i--;\n        }\n      }\n    }\n\n    for (var i = 0; i < bytes.length; i++) {\n      var byte = bytes[i];\n      var value = _codewords[byte];\n      writeValue(value.encodedBytes, value.numBits);\n    }\n\n    if (currentBitOffset < 7) {\n      writeValue(0xff, 1 + currentBitOffset);\n    }\n\n    return buffer.takeBytes();\n  }\n}\n\n/// Specifies the encoding of a specific value using huffman encoding.\nclass EncodedHuffmanValue {\n  /// An integer representation of the encoded bit-string.\n  final int encodedBytes;\n\n  /// The number of bits in [encodedBytes].\n  final int numBits;\n\n  const EncodedHuffmanValue(this.encodedBytes, this.numBits);\n}\n\n/// A node in the huffman tree.\nclass HuffmanTreeNode {\n  HuffmanTreeNode? left;\n  HuffmanTreeNode? right;\n  int? value;\n}\n\n/// Generates a huffman decoding tree.\nHuffmanTreeNode generateHuffmanTree(List<EncodedHuffmanValue> valueEncodings) {\n  var root = HuffmanTreeNode();\n\n  for (var byteOffset = 0; byteOffset < valueEncodings.length; byteOffset++) {\n    var entry = valueEncodings[byteOffset];\n\n    var current = root;\n    for (var bitNr = 0; bitNr < entry.numBits; bitNr++) {\n      var right =\n          ((entry.encodedBytes >> (entry.numBits - bitNr - 1)) & 1) == 1;\n\n      if (right) {\n        current.right ??= HuffmanTreeNode();\n        current = current.right!;\n      } else {\n        current.left ??= HuffmanTreeNode();\n        current = current.left!;\n      }\n    }\n\n    current.value = byteOffset;\n  }\n\n  return root;\n}\n"
  },
  {
    "path": "lib/network/http/h2/hpack/huffman_table.dart",
    "content": "// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file\n// for details. All rights reserved. Use of this source code is governed by a\n// BSD-style license that can be found in the LICENSE file.\n\nimport 'huffman.dart';\n\n/// The huffman codec for encoding/decoding HTTP/2 header blocks.\nfinal HuffmanCodec http2HuffmanCodec = HuffmanCodec(\n  HuffmanEncoder(_codeWords),\n  HuffmanDecoder(generateHuffmanTree(_codeWords)),\n);\n\n/// This is the integer representing the End-of-String symbol\n/// (it is not representable by a byte).\nconst int EOS_BYTE = 256;\n\n/// This list of byte encodings via huffman encoding was generated from the\n/// HPACK specification.\nconst List<EncodedHuffmanValue> _codeWords = <EncodedHuffmanValue>[\n  EncodedHuffmanValue(0x1ff8, 13),\n  EncodedHuffmanValue(0x7fffd8, 23),\n  EncodedHuffmanValue(0xfffffe2, 28),\n  EncodedHuffmanValue(0xfffffe3, 28),\n  EncodedHuffmanValue(0xfffffe4, 28),\n  EncodedHuffmanValue(0xfffffe5, 28),\n  EncodedHuffmanValue(0xfffffe6, 28),\n  EncodedHuffmanValue(0xfffffe7, 28),\n  EncodedHuffmanValue(0xfffffe8, 28),\n  EncodedHuffmanValue(0xffffea, 24),\n  EncodedHuffmanValue(0x3ffffffc, 30),\n  EncodedHuffmanValue(0xfffffe9, 28),\n  EncodedHuffmanValue(0xfffffea, 28),\n  EncodedHuffmanValue(0x3ffffffd, 30),\n  EncodedHuffmanValue(0xfffffeb, 28),\n  EncodedHuffmanValue(0xfffffec, 28),\n  EncodedHuffmanValue(0xfffffed, 28),\n  EncodedHuffmanValue(0xfffffee, 28),\n  EncodedHuffmanValue(0xfffffef, 28),\n  EncodedHuffmanValue(0xffffff0, 28),\n  EncodedHuffmanValue(0xffffff1, 28),\n  EncodedHuffmanValue(0xffffff2, 28),\n  EncodedHuffmanValue(0x3ffffffe, 30),\n  EncodedHuffmanValue(0xffffff3, 28),\n  EncodedHuffmanValue(0xffffff4, 28),\n  EncodedHuffmanValue(0xffffff5, 28),\n  EncodedHuffmanValue(0xffffff6, 28),\n  EncodedHuffmanValue(0xffffff7, 28),\n  EncodedHuffmanValue(0xffffff8, 28),\n  EncodedHuffmanValue(0xffffff9, 28),\n  EncodedHuffmanValue(0xffffffa, 28),\n  EncodedHuffmanValue(0xffffffb, 28),\n  EncodedHuffmanValue(0x14, 6),\n  EncodedHuffmanValue(0x3f8, 10),\n  EncodedHuffmanValue(0x3f9, 10),\n  EncodedHuffmanValue(0xffa, 12),\n  EncodedHuffmanValue(0x1ff9, 13),\n  EncodedHuffmanValue(0x15, 6),\n  EncodedHuffmanValue(0xf8, 8),\n  EncodedHuffmanValue(0x7fa, 11),\n  EncodedHuffmanValue(0x3fa, 10),\n  EncodedHuffmanValue(0x3fb, 10),\n  EncodedHuffmanValue(0xf9, 8),\n  EncodedHuffmanValue(0x7fb, 11),\n  EncodedHuffmanValue(0xfa, 8),\n  EncodedHuffmanValue(0x16, 6),\n  EncodedHuffmanValue(0x17, 6),\n  EncodedHuffmanValue(0x18, 6),\n  EncodedHuffmanValue(0x0, 5),\n  EncodedHuffmanValue(0x1, 5),\n  EncodedHuffmanValue(0x2, 5),\n  EncodedHuffmanValue(0x19, 6),\n  EncodedHuffmanValue(0x1a, 6),\n  EncodedHuffmanValue(0x1b, 6),\n  EncodedHuffmanValue(0x1c, 6),\n  EncodedHuffmanValue(0x1d, 6),\n  EncodedHuffmanValue(0x1e, 6),\n  EncodedHuffmanValue(0x1f, 6),\n  EncodedHuffmanValue(0x5c, 7),\n  EncodedHuffmanValue(0xfb, 8),\n  EncodedHuffmanValue(0x7ffc, 15),\n  EncodedHuffmanValue(0x20, 6),\n  EncodedHuffmanValue(0xffb, 12),\n  EncodedHuffmanValue(0x3fc, 10),\n  EncodedHuffmanValue(0x1ffa, 13),\n  EncodedHuffmanValue(0x21, 6),\n  EncodedHuffmanValue(0x5d, 7),\n  EncodedHuffmanValue(0x5e, 7),\n  EncodedHuffmanValue(0x5f, 7),\n  EncodedHuffmanValue(0x60, 7),\n  EncodedHuffmanValue(0x61, 7),\n  EncodedHuffmanValue(0x62, 7),\n  EncodedHuffmanValue(0x63, 7),\n  EncodedHuffmanValue(0x64, 7),\n  EncodedHuffmanValue(0x65, 7),\n  EncodedHuffmanValue(0x66, 7),\n  EncodedHuffmanValue(0x67, 7),\n  EncodedHuffmanValue(0x68, 7),\n  EncodedHuffmanValue(0x69, 7),\n  EncodedHuffmanValue(0x6a, 7),\n  EncodedHuffmanValue(0x6b, 7),\n  EncodedHuffmanValue(0x6c, 7),\n  EncodedHuffmanValue(0x6d, 7),\n  EncodedHuffmanValue(0x6e, 7),\n  EncodedHuffmanValue(0x6f, 7),\n  EncodedHuffmanValue(0x70, 7),\n  EncodedHuffmanValue(0x71, 7),\n  EncodedHuffmanValue(0x72, 7),\n  EncodedHuffmanValue(0xfc, 8),\n  EncodedHuffmanValue(0x73, 7),\n  EncodedHuffmanValue(0xfd, 8),\n  EncodedHuffmanValue(0x1ffb, 13),\n  EncodedHuffmanValue(0x7fff0, 19),\n  EncodedHuffmanValue(0x1ffc, 13),\n  EncodedHuffmanValue(0x3ffc, 14),\n  EncodedHuffmanValue(0x22, 6),\n  EncodedHuffmanValue(0x7ffd, 15),\n  EncodedHuffmanValue(0x3, 5),\n  EncodedHuffmanValue(0x23, 6),\n  EncodedHuffmanValue(0x4, 5),\n  EncodedHuffmanValue(0x24, 6),\n  EncodedHuffmanValue(0x5, 5),\n  EncodedHuffmanValue(0x25, 6),\n  EncodedHuffmanValue(0x26, 6),\n  EncodedHuffmanValue(0x27, 6),\n  EncodedHuffmanValue(0x6, 5),\n  EncodedHuffmanValue(0x74, 7),\n  EncodedHuffmanValue(0x75, 7),\n  EncodedHuffmanValue(0x28, 6),\n  EncodedHuffmanValue(0x29, 6),\n  EncodedHuffmanValue(0x2a, 6),\n  EncodedHuffmanValue(0x7, 5),\n  EncodedHuffmanValue(0x2b, 6),\n  EncodedHuffmanValue(0x76, 7),\n  EncodedHuffmanValue(0x2c, 6),\n  EncodedHuffmanValue(0x8, 5),\n  EncodedHuffmanValue(0x9, 5),\n  EncodedHuffmanValue(0x2d, 6),\n  EncodedHuffmanValue(0x77, 7),\n  EncodedHuffmanValue(0x78, 7),\n  EncodedHuffmanValue(0x79, 7),\n  EncodedHuffmanValue(0x7a, 7),\n  EncodedHuffmanValue(0x7b, 7),\n  EncodedHuffmanValue(0x7ffe, 15),\n  EncodedHuffmanValue(0x7fc, 11),\n  EncodedHuffmanValue(0x3ffd, 14),\n  EncodedHuffmanValue(0x1ffd, 13),\n  EncodedHuffmanValue(0xffffffc, 28),\n  EncodedHuffmanValue(0xfffe6, 20),\n  EncodedHuffmanValue(0x3fffd2, 22),\n  EncodedHuffmanValue(0xfffe7, 20),\n  EncodedHuffmanValue(0xfffe8, 20),\n  EncodedHuffmanValue(0x3fffd3, 22),\n  EncodedHuffmanValue(0x3fffd4, 22),\n  EncodedHuffmanValue(0x3fffd5, 22),\n  EncodedHuffmanValue(0x7fffd9, 23),\n  EncodedHuffmanValue(0x3fffd6, 22),\n  EncodedHuffmanValue(0x7fffda, 23),\n  EncodedHuffmanValue(0x7fffdb, 23),\n  EncodedHuffmanValue(0x7fffdc, 23),\n  EncodedHuffmanValue(0x7fffdd, 23),\n  EncodedHuffmanValue(0x7fffde, 23),\n  EncodedHuffmanValue(0xffffeb, 24),\n  EncodedHuffmanValue(0x7fffdf, 23),\n  EncodedHuffmanValue(0xffffec, 24),\n  EncodedHuffmanValue(0xffffed, 24),\n  EncodedHuffmanValue(0x3fffd7, 22),\n  EncodedHuffmanValue(0x7fffe0, 23),\n  EncodedHuffmanValue(0xffffee, 24),\n  EncodedHuffmanValue(0x7fffe1, 23),\n  EncodedHuffmanValue(0x7fffe2, 23),\n  EncodedHuffmanValue(0x7fffe3, 23),\n  EncodedHuffmanValue(0x7fffe4, 23),\n  EncodedHuffmanValue(0x1fffdc, 21),\n  EncodedHuffmanValue(0x3fffd8, 22),\n  EncodedHuffmanValue(0x7fffe5, 23),\n  EncodedHuffmanValue(0x3fffd9, 22),\n  EncodedHuffmanValue(0x7fffe6, 23),\n  EncodedHuffmanValue(0x7fffe7, 23),\n  EncodedHuffmanValue(0xffffef, 24),\n  EncodedHuffmanValue(0x3fffda, 22),\n  EncodedHuffmanValue(0x1fffdd, 21),\n  EncodedHuffmanValue(0xfffe9, 20),\n  EncodedHuffmanValue(0x3fffdb, 22),\n  EncodedHuffmanValue(0x3fffdc, 22),\n  EncodedHuffmanValue(0x7fffe8, 23),\n  EncodedHuffmanValue(0x7fffe9, 23),\n  EncodedHuffmanValue(0x1fffde, 21),\n  EncodedHuffmanValue(0x7fffea, 23),\n  EncodedHuffmanValue(0x3fffdd, 22),\n  EncodedHuffmanValue(0x3fffde, 22),\n  EncodedHuffmanValue(0xfffff0, 24),\n  EncodedHuffmanValue(0x1fffdf, 21),\n  EncodedHuffmanValue(0x3fffdf, 22),\n  EncodedHuffmanValue(0x7fffeb, 23),\n  EncodedHuffmanValue(0x7fffec, 23),\n  EncodedHuffmanValue(0x1fffe0, 21),\n  EncodedHuffmanValue(0x1fffe1, 21),\n  EncodedHuffmanValue(0x3fffe0, 22),\n  EncodedHuffmanValue(0x1fffe2, 21),\n  EncodedHuffmanValue(0x7fffed, 23),\n  EncodedHuffmanValue(0x3fffe1, 22),\n  EncodedHuffmanValue(0x7fffee, 23),\n  EncodedHuffmanValue(0x7fffef, 23),\n  EncodedHuffmanValue(0xfffea, 20),\n  EncodedHuffmanValue(0x3fffe2, 22),\n  EncodedHuffmanValue(0x3fffe3, 22),\n  EncodedHuffmanValue(0x3fffe4, 22),\n  EncodedHuffmanValue(0x7ffff0, 23),\n  EncodedHuffmanValue(0x3fffe5, 22),\n  EncodedHuffmanValue(0x3fffe6, 22),\n  EncodedHuffmanValue(0x7ffff1, 23),\n  EncodedHuffmanValue(0x3ffffe0, 26),\n  EncodedHuffmanValue(0x3ffffe1, 26),\n  EncodedHuffmanValue(0xfffeb, 20),\n  EncodedHuffmanValue(0x7fff1, 19),\n  EncodedHuffmanValue(0x3fffe7, 22),\n  EncodedHuffmanValue(0x7ffff2, 23),\n  EncodedHuffmanValue(0x3fffe8, 22),\n  EncodedHuffmanValue(0x1ffffec, 25),\n  EncodedHuffmanValue(0x3ffffe2, 26),\n  EncodedHuffmanValue(0x3ffffe3, 26),\n  EncodedHuffmanValue(0x3ffffe4, 26),\n  EncodedHuffmanValue(0x7ffffde, 27),\n  EncodedHuffmanValue(0x7ffffdf, 27),\n  EncodedHuffmanValue(0x3ffffe5, 26),\n  EncodedHuffmanValue(0xfffff1, 24),\n  EncodedHuffmanValue(0x1ffffed, 25),\n  EncodedHuffmanValue(0x7fff2, 19),\n  EncodedHuffmanValue(0x1fffe3, 21),\n  EncodedHuffmanValue(0x3ffffe6, 26),\n  EncodedHuffmanValue(0x7ffffe0, 27),\n  EncodedHuffmanValue(0x7ffffe1, 27),\n  EncodedHuffmanValue(0x3ffffe7, 26),\n  EncodedHuffmanValue(0x7ffffe2, 27),\n  EncodedHuffmanValue(0xfffff2, 24),\n  EncodedHuffmanValue(0x1fffe4, 21),\n  EncodedHuffmanValue(0x1fffe5, 21),\n  EncodedHuffmanValue(0x3ffffe8, 26),\n  EncodedHuffmanValue(0x3ffffe9, 26),\n  EncodedHuffmanValue(0xffffffd, 28),\n  EncodedHuffmanValue(0x7ffffe3, 27),\n  EncodedHuffmanValue(0x7ffffe4, 27),\n  EncodedHuffmanValue(0x7ffffe5, 27),\n  EncodedHuffmanValue(0xfffec, 20),\n  EncodedHuffmanValue(0xfffff3, 24),\n  EncodedHuffmanValue(0xfffed, 20),\n  EncodedHuffmanValue(0x1fffe6, 21),\n  EncodedHuffmanValue(0x3fffe9, 22),\n  EncodedHuffmanValue(0x1fffe7, 21),\n  EncodedHuffmanValue(0x1fffe8, 21),\n  EncodedHuffmanValue(0x7ffff3, 23),\n  EncodedHuffmanValue(0x3fffea, 22),\n  EncodedHuffmanValue(0x3fffeb, 22),\n  EncodedHuffmanValue(0x1ffffee, 25),\n  EncodedHuffmanValue(0x1ffffef, 25),\n  EncodedHuffmanValue(0xfffff4, 24),\n  EncodedHuffmanValue(0xfffff5, 24),\n  EncodedHuffmanValue(0x3ffffea, 26),\n  EncodedHuffmanValue(0x7ffff4, 23),\n  EncodedHuffmanValue(0x3ffffeb, 26),\n  EncodedHuffmanValue(0x7ffffe6, 27),\n  EncodedHuffmanValue(0x3ffffec, 26),\n  EncodedHuffmanValue(0x3ffffed, 26),\n  EncodedHuffmanValue(0x7ffffe7, 27),\n  EncodedHuffmanValue(0x7ffffe8, 27),\n  EncodedHuffmanValue(0x7ffffe9, 27),\n  EncodedHuffmanValue(0x7ffffea, 27),\n  EncodedHuffmanValue(0x7ffffeb, 27),\n  EncodedHuffmanValue(0xffffffe, 28),\n  EncodedHuffmanValue(0x7ffffec, 27),\n  EncodedHuffmanValue(0x7ffffed, 27),\n  EncodedHuffmanValue(0x7ffffee, 27),\n  EncodedHuffmanValue(0x7ffffef, 27),\n  EncodedHuffmanValue(0x7fffff0, 27),\n  EncodedHuffmanValue(0x3ffffee, 26),\n  EncodedHuffmanValue(0x3fffffff, 30),\n];\n"
  },
  {
    "path": "lib/network/http/h2/setting.dart",
    "content": "/*\n * Copyright 2023 the original author or 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\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/http/h2/frame.dart';\nimport 'package:proxypin/network/util/byte_buf.dart';\n\nclass StreamSetting {\n  /// 允许发送方通知远程端点用于解码头块的头压缩表的最大大小（以八位字节为单位）。\n  /// 初始值为4096个八位字节。\n  int headTableSize = 4096;\n\n  ///如果一个端点接收到的这个参数设置为0，它就不能发送PUSH_PROMISE帧。\n  ///初始值为1，表示允许服务器推送。\n  bool enablePush = true;\n\n  ///指示发送方允许的最大并发流数。这个限制是定向的：它适用于发送方允许接收方创建的流的数量。最初，对该值没有限制。建议此值不小于100，以免不必要地限制并行性。\n  int? maxConcurrentStreams;\n\n  /// 指示发送方用于流级流控制的初始窗口大小（以八位字节为单位）。初始值为216-1（65，535）个八位字节。\n  int initialWindowSize = 65535;\n\n  ///表示发送方愿意接收的最大帧有效负载的大小（以八位字节为单位）。\n  int maxFrameSize = 16384;\n\n  ///建议设置通知对等方发送方准备接受的头列表的最大大小（以八位字节为单位）。\n  ///该值基于头字段的未压缩大小，包括名称和值的长度（以八位字节为单位）加上每个头字段32个八位字节的开销。\n  int? maxHeaderListSize;\n}\n\nclass SettingHandler {\n  static void handleSettingsFrame(ChannelContext channelContext, FrameHeader frameHeader, ByteBuf payload) {\n    // SETTINGS frames must have a length that is a multiple of 6 bytes\n    if (frameHeader.length % 6 != 0) {\n      throw Exception(\"Invalid SETTINGS frame length\");\n    }\n\n    // If the SETTINGS frame has the ACK flag set, then it is an acknowledgement\n    if (frameHeader.hasAckFlag) {\n      // Handle SETTINGS ACK\n      return;\n    }\n    var setting = channelContext.setting ??= StreamSetting();\n    // Otherwise, it is a SETTINGS frame that carries settings\n    while (payload.isReadable()) {\n      int identifier = payload.readShort();\n      int value = payload.readInt();\n      // print(\"SettingHandler.handleSettingsFrame identifier=$identifier value=$value\");\n\n      // Handle the setting based on its identifier\n      switch (identifier) {\n        case 1: // SETTINGS_HEADER_TABLE_SIZE\n          setting.headTableSize = value;\n          break;\n        case 2: // SETTINGS_ENABLE_PUSH\n          setting.enablePush = value == 1;\n          break;\n        case 3: // SETTINGS_MAX_CONCURRENT_STREAMS\n          setting.maxConcurrentStreams = value;\n          break;\n        case 4: // SETTINGS_INITIAL_WINDOW_SIZE\n          setting.initialWindowSize = value;\n          break;\n        case 5: // SETTINGS_MAX_FRAME_SIZE\n          setting.maxFrameSize = value;\n          break;\n        case 6: // SETTINGS_MAX_HEADER_LIST_SIZE\n          setting.maxHeaderListSize = value;\n        default:\n          break;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/network/http/http.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:math';\n\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/content_type.dart';\nimport 'package:proxypin/network/http/websocket.dart';\nimport 'package:proxypin/network/util/compress.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/process_info.dart';\n\nimport 'http_headers.dart';\n\n///定义HTTP消息的接口，为HttpRequest和HttpResponse提供公共属性。\n///@author WangHongEn\nabstract class HttpMessage {\n  /// HTTP/1.1\n  static const String http1Version = \"HTTP/1.1\";\n\n  ///内容类型\n  static final Map<String, ContentType> contentTypes = {\n    \"javascript\": ContentType.js,\n    \"text/css\": ContentType.css,\n    \"font-woff\": ContentType.font,\n    \"text/html\": ContentType.html,\n    \"text/plain\": ContentType.text,\n    \"application/x-www-form-urlencoded\": ContentType.formUrl,\n    \"form-data\": ContentType.formData,\n    \"image\": ContentType.image,\n    \"video\": ContentType.video,\n    \"application/json\": ContentType.json,\n    \"text/event-stream\": ContentType.sse,\n  };\n\n  String protocolVersion;\n\n  final HttpHeaders headers = HttpHeaders();\n\n  int get contentLength => headers.contentLength;\n\n  String? get requestUrl;\n\n  //报文大小\n  int? packageSize;\n\n  List<int>? _body;\n  String? _bodyString;\n\n  String? remoteHost;\n  int? remotePort;\n\n  String requestId = (DateTime.now().millisecondsSinceEpoch + Random().nextInt(999999)).toRadixString(36);\n  int? streamId; // http2 streamId\n  HttpMessage(this.protocolVersion);\n\n  //json序列化\n  factory HttpMessage.fromJson(Map<String, dynamic> json) {\n    if (json[\"_class\"] == \"HttpRequest\") {\n      return HttpRequest.fromJson(json);\n    }\n\n    return HttpResponse.fromJson(json);\n  }\n\n  Map<String, dynamic> toJson();\n\n  /// 是否是websocket协议\n  bool get isWebSocket => headers.get(\"Upgrade\") == 'websocket';\n\n  ContentType get contentType => contentTypes.entries\n      .firstWhere((element) => headers.contentType.contains(element.key),\n          orElse: () => const MapEntry(\"unknown\", ContentType.http))\n      .value;\n\n  List<int>? get body => _body;\n\n  set body(List<int>? body) {\n    _body = body;\n    _bodyString = null;\n  }\n\n  ///获取消息体编码\n  String? get charset {\n    var contentType = headers.contentType;\n    if (contentType.isEmpty) {\n      return 'utf-8';\n    }\n\n    MediaType? mediaType = MediaType.valueOf(contentType);\n    if (mediaType == null) {\n      return 'utf-8';\n    }\n    return mediaType.charset ?? MediaType.defaultCharset(mediaType);\n  }\n\n  ///获取消息\n  String get bodyAsString {\n    return getBodyString(charset: 'utf-8');\n  }\n\n  String getBodyString({String? charset}) {\n    if (body == null || body?.isEmpty == true) {\n      return \"\";\n    }\n\n    if (_bodyString != null) {\n      return _bodyString!;\n    }\n\n    charset ??= this.charset;\n    try {\n      List<int> rawBody = body!;\n\n      if (headers.isGzip) {\n        rawBody = gzipDecode(body!);\n      }else\n\n      if (headers.contentEncoding == 'br') {\n        rawBody = brDecode(body!);\n      } else if  (headers.contentEncoding == 'deflate') {\n        rawBody = zlibDecode(body!);\n      }\n\n      if (charset == 'utf-8' || charset == 'utf8') {\n        return utf8.decode(rawBody);\n      }\n\n      return String.fromCharCodes(rawBody);\n    } catch (e) {\n      return String.fromCharCodes(body!);\n    }\n  }\n\n  Future<String> decodeBodyString() async {\n    if (body == null || body?.isEmpty == true) {\n      return \"\";\n    }\n\n    if (_bodyString != null) {\n      return _bodyString!;\n    }\n\n    List<int> rawBody = body!;\n    if (headers.contentEncoding == 'zstd') {\n      rawBody = await zstdDecode(body!) ?? [];\n      if (charset == 'utf-8' || charset == 'utf8') {\n        _bodyString = utf8.decode(rawBody);\n      } else {\n        _bodyString = String.fromCharCodes(rawBody);\n      }\n      return _bodyString!;\n    }\n\n    return getBodyString();\n  }\n\n  List<String> get cookies => headers.cookies;\n\n  List<WebSocketFrame> messages = [];\n}\n\n///HTTP请求。\nclass HttpRequest extends HttpMessage {\n  String _uri;\n  HttpMethod method;\n\n  HostAndPort? hostAndPort;\n  DateTime requestTime = DateTime.now(); //请求时间\n  HttpResponse? response;\n  Map<String, dynamic> attributes = {};\n  ProcessInfo? processInfo;\n\n  String get uri => _uri;\n\n  set uri(String uri) {\n    _uri = uri;\n    _requestUri = null;\n  }\n\n  HttpRequest(this.method, this._uri, {String protocolVersion = \"HTTP/1.1\"}) : super(protocolVersion);\n\n  String? remoteDomain() {\n    if (hostAndPort == null && HostAndPort.startsWithScheme(uri)) {\n      try {\n        var uri = Uri.parse(this.uri);\n        return '${uri.scheme}://${uri.host}${uri.hasPort ? ':${uri.port}' : ''}';\n      } catch (e) {\n        return null;\n      }\n    }\n\n    return hostAndPort?.domain;\n  }\n\n  @override\n  String get requestUrl {\n    if (HostAndPort.startsWithScheme(uri)) {\n      return uri;\n    }\n\n    if (method == HttpMethod.connect) {\n      return \"${hostAndPort?.scheme ?? 'http://'}$uri\";\n    }\n\n    return '${remoteDomain()}$uri';\n  }\n\n  /// 请求的uri\n  Uri? _requestUri;\n\n  Uri? get requestUri {\n    try {\n      _requestUri ??= Uri.parse(requestUrl);\n      return _requestUri;\n    } catch (e) {\n      logger.w('parse uri error $requestUrl  ${hostAndPort?.scheme} ${hostAndPort?.host}: $e');\n      return null;\n    }\n  }\n\n  ///域名+路径\n  String get domainPath => '${remoteDomain()}$path';\n\n  /// 请求的path\n  String get path => requestUri?.path ?? '';\n\n  /// path and query\n  String get pathAndQuery => '${requestUri?.path}${requestUri?.hasQuery == true ? '?${requestUri?.query}' : ''}';\n\n  Map<String, String> get queries => requestUri?.queryParameters ?? {};\n\n  ///获取消息体编码\n  @override\n  String? get charset {\n    return super.charset ?? 'utf-8';\n  }\n\n  ///复制请求\n  HttpRequest copy({String? uri}) {\n    var request = HttpRequest(method, uri ?? this.uri, protocolVersion: protocolVersion);\n    request.headers.addAll(headers);\n    if (uri != null && !uri.startsWith('/')) {\n      request.hostAndPort = HostAndPort.of(uri);\n    }\n    request.hostAndPort ??= hostAndPort;\n    request.streamId = streamId;\n    request.body = body;\n    request.messages = messages;\n    return request;\n  }\n\n  @override\n  Map<String, dynamic> toJson() {\n    return {\n      '_class': 'HttpRequest',\n      '_id': requestId,\n      'uri': requestUrl,\n      'method': method.name,\n      'protocolVersion': protocolVersion,\n      'packageSize': packageSize,\n      'headers': headers.toJson(),\n      'body': body == null ? null : String.fromCharCodes(body!),\n      'requestTime': requestTime.millisecondsSinceEpoch,\n      'messages': messages.map((e) => e.toJson()).toList(),\n    };\n  }\n\n  factory HttpRequest.fromJson(Map<String, dynamic> json) {\n    var request = HttpRequest(HttpMethod.valueOf(json['method']), json['uri'],\n        protocolVersion: json['protocolVersion'] ?? \"HTTP/1.1\");\n\n    request.requestId = json['_id'] ?? request.requestId;\n    request.headers.addAll(HttpHeaders.fromJson(json['headers']));\n    request.body = json['body']?.toString().codeUnits;\n    if (json['requestTime'] != null) {\n      request.requestTime = DateTime.fromMillisecondsSinceEpoch(json['requestTime']);\n    }\n\n    if (json['messages'] is List) {\n      request.messages = (json['messages'] as List)\n          .whereType<Map>()\n          .map((e) => WebSocketFrame.fromJson(Map<String, dynamic>.from(e)))\n          .toList();\n    }\n    request.packageSize = json['packageSize'];\n    return request;\n  }\n\n  @override\n  String toString() {\n    return 'HttpRequest{version: $protocolVersion, uri: $uri, method: ${method.name}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}';\n  }\n}\n\n///HTTP响应。\nclass HttpResponse extends HttpMessage {\n  HttpStatus status;\n  DateTime responseTime = DateTime.now();\n  HttpRequest? request;\n  String? _requestUrl;\n\n  @override\n  String? get requestUrl => request?.requestUrl ?? _requestUrl;\n\n  HttpResponse(this.status, {String protocolVersion = \"HTTP/1.1\"}) : super(protocolVersion);\n\n  /// 复制响应\n  HttpResponse copy() {\n    var response = HttpResponse(status, protocolVersion: protocolVersion);\n    response.headers.addAll(headers);\n    response.body = body;\n    response.request = request;\n    response.messages = messages;\n    return response;\n  }\n\n  String costTime() {\n    if (request == null) {\n      return '';\n    }\n    var cost = responseTime.difference(request!.requestTime).inMilliseconds;\n    if (cost > 1000) {\n      return '${(cost / 1000).toStringAsFixed(2)}s';\n    }\n    return '${cost}ms';\n  }\n\n  //json序列化\n  factory HttpResponse.fromJson(Map<String, dynamic> json) {\n    var httpResponse = HttpResponse(HttpStatus(json['status']['code'], json['status']['reasonPhrase']),\n        protocolVersion: json['protocolVersion'])\n      ..headers.addAll(HttpHeaders.fromJson(json['headers']))\n      ..body = json['body']?.toString().codeUnits;\n    if (json['responseTime'] != null) {\n      httpResponse.responseTime = DateTime.fromMillisecondsSinceEpoch(json['responseTime']);\n    }\n    if (json['messages'] is List) {\n      httpResponse.messages = (json['messages'] as List)\n          .where((e) => e is Map)\n          .map((e) => WebSocketFrame.fromJson(Map<String, dynamic>.from(e)))\n          .toList();\n    }\n    httpResponse.packageSize = json['packageSize'];\n    httpResponse._requestUrl = json['requestUrl'];\n    return httpResponse;\n  }\n\n  @override\n  Map<String, dynamic> toJson() {\n    return {\n      '_class': 'HttpResponse',\n      'requestUrl': request?.requestUrl ?? _requestUrl,\n      'protocolVersion': protocolVersion,\n      'packageSize': packageSize,\n      'status': {\n        'code': status.code,\n        'reasonPhrase': status.reasonPhrase,\n      },\n      'headers': headers.toJson(),\n      'body': body == null ? null : String.fromCharCodes(body!),\n      'responseTime': responseTime.millisecondsSinceEpoch,\n      'messages': messages.map((e) => e.toJson()).toList(),\n    };\n  }\n\n  @override\n  String toString() {\n    return 'HttpResponse{status: ${status.code}, protocolVersion: $protocolVersion headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}';\n  }\n}\n\n///HTTP请求方法。\nenum HttpMethod {\n  get(\"GET\"),\n  post(\"POST\"),\n  put(\"PUT\"),\n  patch(\"PATCH\"),\n  delete(\"DELETE\"),\n  options(\"OPTIONS\"),\n  head(\"HEAD\"),\n  trace(\"TRACE\"),\n  connect(\"CONNECT\"),\n  propfind(\"PROPFIND\"),\n  report(\"REPORT\"),\n  ;\n\n  final String name;\n\n  const HttpMethod(this.name);\n\n  static HttpMethod valueOf(String name) {\n    try {\n      return HttpMethod.values.firstWhere((element) => element.name == name.toUpperCase());\n    } catch (error) {\n      logger.e(\"HttpMethod error $name :$error\");\n      rethrow;\n    }\n  }\n\n  static List<HttpMethod> methods() {\n    return values.where((method) => method != HttpMethod.propfind && method != HttpMethod.report).toList();\n  }\n}\n\n///HTTP响应状态。\nclass HttpStatus {\n  /// 200 OK\n  static final HttpStatus ok = newStatus(200, \"OK\");\n\n  /// 400 Bad Request\n  static final HttpStatus badRequest = newStatus(400, \"Bad Request\");\n\n  /// 401 Unauthorized\n  static final HttpStatus unauthorized = newStatus(401, \"Unauthorized\");\n\n  /// 403 Forbidden\n  static final HttpStatus forbidden = newStatus(403, \"Forbidden\");\n\n  /// 404 Not Found\n  static final HttpStatus notFound = newStatus(404, \"Not Found\");\n\n  /// 500 Internal Server Error\n  static final HttpStatus internalServerError = newStatus(500, \"Internal Server Error\");\n\n  /// 502 Bad Gateway\n  static final HttpStatus badGateway = newStatus(502, \"Bad Gateway\");\n\n  /// 503 Service Unavailable\n  static final HttpStatus serviceUnavailable = newStatus(503, \"Service Unavailable\");\n\n  /// 504 Gateway Timeout\n  static final HttpStatus gatewayTimeout = newStatus(504, \"Gateway Timeout\");\n\n  static HttpStatus newStatus(int statusCode, String? reasonPhrase) {\n    if (reasonPhrase == null) {\n      return HttpStatus.valueOf(statusCode);\n    }\n\n    return HttpStatus(statusCode, reasonPhrase);\n  }\n\n  static HttpStatus valueOf(int code) {\n    switch (code) {\n      case 200:\n        return ok;\n      case 400:\n        return badRequest;\n      case 401:\n        return unauthorized;\n      case 403:\n        return forbidden;\n      case 404:\n        return notFound;\n      case 500:\n        return internalServerError;\n      case 502:\n        return badGateway;\n      case 503:\n        return serviceUnavailable;\n      case 504:\n        return gatewayTimeout;\n    }\n    return HttpStatus(code, \"\");\n  }\n\n  final int code;\n  String reasonPhrase;\n\n  HttpStatus reason(String reasonPhrase) {\n    this.reasonPhrase = reasonPhrase;\n    return this;\n  }\n\n  HttpStatus(this.code, this.reasonPhrase);\n\n  bool isSuccessful() {\n    return code >= 200 && code < 300;\n  }\n\n  @override\n  String toString() {\n    return '$code  $reasonPhrase';\n  }\n}\n"
  },
  {
    "path": "lib/network/http/http_client.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/h2/h2_codec.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\nimport 'package:proxypin/network/channel/network.dart';\nimport 'package:proxypin/network/util/byte_buf.dart';\nimport 'package:proxypin/network/util/byte_utils.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/system_proxy.dart';\nimport 'package:proxy_manager/proxy_manager.dart';\n\nimport '../channel/channel.dart';\nimport 'codec.dart';\nimport 'h2/frame.dart';\nimport 'h2/setting.dart';\n\nclass HttpClients {\n  static Future<Channel> startConnect(HostAndPort hostAndPort, {Duration timeout = const Duration(seconds: 3)}) {\n    String host = hostAndPort.host;\n    //说明支持ipv6\n    if (host.startsWith(\"[\") && host.endsWith(']')) {\n      host = host.substring(1, host.length - 1);\n    }\n\n    return Socket.connect(host, hostAndPort.port, timeout: timeout).then((socket) {\n      if (socket.address.type != InternetAddressType.unix) {\n        socket.setOption(SocketOption.tcpNoDelay, true);\n      }\n      return Channel(socket);\n    });\n  }\n\n  ///代理建立连接\n  static Future<Channel> proxyConnect(\n      HttpRequest request, HostAndPort hostAndPort, ChannelHandler<HttpResponse> handler, ChannelContext channelContext,\n      {ProxyInfo? proxyInfo}) async {\n    var client = Client()..initChannel((channel) => channel.dispatcher.channelHandle(HttpClientCodec(), handler));\n\n    if (proxyInfo == null) {\n      var proxyTypes = hostAndPort.isSsl() ? ProxyTypes.https : ProxyTypes.http;\n      proxyInfo = await SystemProxy.getSystemProxy(proxyTypes);\n    }\n\n    HostAndPort connectHost = proxyInfo == null ? hostAndPort : HostAndPort.host(proxyInfo.host, proxyInfo.port!);\n    var channel = await client.connect(connectHost, channelContext);\n\n    if (proxyInfo != null) {\n      await connectRequest(channelContext, hostAndPort, channel, proxyInfo: proxyInfo);\n    }\n\n    if (hostAndPort.isSsl()) {\n      await channel.startSecureSocket(channelContext,\n          host: hostAndPort.host, supportedProtocols: request.protocolVersion == \"HTTP/2\" ? [\"h2\", \"http/1.1\"] : null);\n      if (channelContext.serverChannel?.selectedProtocol == \"h2\") {\n        await Http2ClientHandler(handler).listen(channel, channelContext);\n      } else {\n        request.protocolVersion = \"HTTP/1.1\";\n        channel.dispatcher.listen(channel, channelContext);\n      }\n    }\n\n    logger.d(\n        \"request ${hostAndPort.host}:${hostAndPort.port} ${request.protocolVersion} ${channelContext.serverChannel?.selectedProtocol ?? ''}\");\n\n    return channel;\n  }\n\n  ///发起代理连接请求\n  static Future<Channel> connectRequest(ChannelContext channelContext, HostAndPort hostAndPort, Channel channel,\n      {ProxyInfo? proxyInfo}) async {\n    ChannelHandler handler = channel.dispatcher.handler;\n    //代理 发送connect请求\n    var httpResponseHandler = HttpResponseHandler();\n    channel.dispatcher.handler = httpResponseHandler;\n\n    HttpRequest proxyRequest = HttpRequest(HttpMethod.connect, '${hostAndPort.host}:${hostAndPort.port}');\n    proxyRequest.headers.set(HttpHeaders.HOST, '${hostAndPort.host}:${hostAndPort.port}');\n\n    //proxy Authorization\n    if (proxyInfo?.isAuthenticated == true) {\n      String auth = base64Encode(utf8.encode(\"${proxyInfo?.username}:${proxyInfo?.password}\"));\n      proxyRequest.headers.set(HttpHeaders.PROXY_AUTHORIZATION, 'Basic $auth');\n    }\n\n    await channel.write(channelContext, proxyRequest);\n    var response = await httpResponseHandler.getResponse(const Duration(seconds: 5));\n\n    channel.dispatcher.handler = handler;\n\n    if (!response.status.isSuccessful()) {\n      throw Exception(\"$hostAndPort Proxy failed to establish tunnel \"\n          \"(${response.status.code} ${response..status.reasonPhrase})\");\n    }\n\n    return channel;\n  }\n\n  /// 建立连接\n  static Future<Channel> connect(Uri uri, ChannelHandler handler, ChannelContext channelContext) async {\n    Client client = Client()\n      ..initChannel((channel) => channel.dispatcher.handle(HttpResponseCodec(), HttpRequestCodec(), handler));\n    if (uri.scheme == \"https\" || uri.scheme == \"wss\") {\n      return client.secureConnect(HostAndPort.of(uri.toString()), channelContext);\n    }\n\n    return client.connect(HostAndPort.of(uri.toString()), channelContext);\n  }\n\n  /// 发送get请求\n  static Future<HttpResponse> get(String url, {Duration timeout = const Duration(seconds: 3)}) async {\n    HttpRequest msg = HttpRequest(HttpMethod.get, url);\n    return request(HostAndPort.of(url), msg, timeout: timeout);\n  }\n\n  /// 发送请求\n  static Future<HttpResponse> request(HostAndPort hostAndPort, HttpRequest request,\n      {Duration timeout = const Duration(seconds: 3)}) async {\n    var httpResponseHandler = HttpResponseHandler();\n\n    var client = Client()\n      ..initChannel(\n          (channel) => channel.dispatcher.handle(HttpResponseCodec(), HttpRequestCodec(), httpResponseHandler));\n\n    ChannelContext channelContext = ChannelContext();\n    Channel channel = await client.connect(hostAndPort, channelContext);\n    await channel.write(channelContext, request);\n\n    return httpResponseHandler.getResponse(timeout).whenComplete(() => channel.close());\n  }\n\n  /// 发送代理请求\n  static Future<HttpResponse> proxyRequest(HttpRequest request,\n      {ProxyInfo? proxyInfo, Duration timeout = const Duration(seconds: 30)}) async {\n    if (request.headers.host == null || request.headers.host?.trim().isEmpty == true) {\n      try {\n        var uri = Uri.parse(request.requestUrl);\n        request.headers.host = '${uri.host}${uri.hasPort ? ':${uri.port}' : ''}';\n      } catch (_) {}\n    }\n\n    ChannelContext channelContext = ChannelContext();\n    var httpResponseHandler = HttpResponseHandler();\n    request.hostAndPort ??= HostAndPort.of(request.requestUrl);\n\n    Channel channel =\n        await proxyConnect(request, proxyInfo: proxyInfo, request.hostAndPort!, httpResponseHandler, channelContext);\n\n    if (!request.uri.startsWith(\"/\")) {\n      Uri? uri = request.requestUri;\n      request = request.copy(uri: '${uri!.path}${uri.hasQuery ? '?${uri.query}' : ''}');\n    }\n\n    if (channel.selectedProtocol == 'h2') {\n      request.headers.remove(HttpHeaders.HOST);\n      request.streamId = 1;\n    }\n    await channel.write(channelContext, request);\n    return httpResponseHandler.getResponse(timeout).whenComplete(() => channel.close());\n  }\n}\n\nclass Http2ClientHandler {\n  static const int FLAG_ACK = 0x1;\n\n  ByteBuf byteBuf = ByteBuf();\n  Http2ResponseDecoder decoder = Http2ResponseDecoder();\n  final ChannelHandler<HttpResponse> handler;\n\n  Http2ClientHandler(this.handler);\n\n  Future<void> listen(Channel channel, ChannelContext channelContext) async {\n    channel.dispatcher.encoder = Http2RequestDecoder();\n    channel.dispatcher.decoder = decoder;\n\n    channel.socket.listen((data) => onData(channelContext, channel, data),\n        onError: (error, trace) => handler.exceptionCaught(channelContext, channel, error, trace: trace),\n        onDone: () => handler.channelInactive(channelContext, channel));\n\n    await channel.writeBytes(Http2Codec.connectionPrefacePRI);\n\n    //发送setting\n    final streamSetting = StreamSetting();\n    streamSetting.headTableSize = 65536;\n    streamSetting.initialWindowSize = 1048896;\n    streamSetting.maxHeaderListSize = 262144;\n\n    var payload = Uint8List(6 * 3);\n    int offset = 0;\n    // SETTINGS_HEADER_TABLE_SIZE\n    setInt16(payload, offset, 1);\n    offset += 2;\n    setInt32(payload, offset, streamSetting.headTableSize);\n    offset += 4;\n\n    // SETTINGS_INITIAL_WINDOW_SIZE\n    setInt16(payload, offset, 4);\n    offset += 2;\n    setInt32(payload, offset, streamSetting.initialWindowSize);\n    offset += 4;\n\n    //SETTINGS_MAX_FRAME_SIZE\n    setInt16(payload, offset, 6);\n    offset += 2;\n    setInt32(payload, offset, streamSetting.maxHeaderListSize!);\n    offset += 4;\n\n    var settingFrame = FrameHeader(payload.length, FrameType.settings, 0, 0);\n    var buffer = settingFrame.encode()..addAll(payload);\n    await channel.writeBytes(buffer);\n  }\n\n  void onData(ChannelContext channelContext, Channel channel, Uint8List data) {\n    byteBuf.add(data);\n    var decodeResult = decoder.decode(channelContext, byteBuf);\n\n    if (!decodeResult.isDone) {\n      return;\n    }\n\n    byteBuf.clearRead();\n\n    if (decodeResult.forward != null) {\n      ByteBuf buffer = ByteBuf(decodeResult.forward);\n\n      FrameHeader? frameHeader = FrameReader.readFrameHeader(buffer);\n      logger.d(\"Http2ClientHandler forward ${frameHeader?.type}\");\n      if (frameHeader?.type == FrameType.settings) {\n        // 检查是否需要发送 ACK\n        if (frameHeader!.hasAckFlag == false) {\n          // 发送带有 ACK 标志的 SETTINGS 帧\n          var ackFrame = FrameHeader(0, FrameType.settings, FLAG_ACK, 0);\n          channel.writeBytes(ackFrame.encode());\n        }\n      }\n\n      return;\n    }\n\n    handler.channelRead(channelContext, channel, decodeResult.data!);\n  }\n}\n\nclass HttpResponseHandler extends ChannelHandler<HttpResponse> {\n  Completer<HttpResponse> _completer = Completer<HttpResponse>();\n\n  @override\n  Future<void> channelRead(ChannelContext channelContext, Channel channel, HttpResponse msg) async {\n    // log.i(\"[${channel.id}] Response $msg\");\n    _completer.complete(msg);\n  }\n\n  Future<HttpResponse> getResponse(Duration duration) {\n    return _completer.future.timeout(duration);\n  }\n\n  void resetResponse() {\n    _completer = Completer<HttpResponse>();\n  }\n}\n"
  },
  {
    "path": "lib/network/http/http_headers.dart",
    "content": "// ignore_for_file: constant_identifier_names\n\n/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:collection';\n\nclass HttpHeaders {\n  static const String CONTENT_LENGTH = \"Content-Length\";\n  static const String CONTENT_ENCODING = \"Content-Encoding\";\n  static const String CONTENT_TYPE = \"Content-Type\";\n  static const String HOST = \"Host\";\n  static const String TRANSFER_ENCODING = \"Transfer-Encoding\";\n  static const String Cookie = \"Cookie\";\n  static const String PROXY_AUTHORIZATION = \"Proxy-Authorization\";\n\n  static const List<String> commonHeaderKeys = [\n    'Accept',\n    'Accept-Charset',\n    'Accept-Encoding',\n    'Accept-Language',\n    'Accept-Ranges',\n    'Authorization',\n    'Cache-Control',\n    'Connection',\n    'Content-Type',\n    'Content-Length',\n    'Content-Encoding',\n    'Cookie',\n    'Date',\n    'Expect',\n    'From',\n    'Host',\n    'If-Match',\n    'If-Modified-Since',\n    'If-None-Match',\n    'If-Range',\n    'If-Unmodified-Since',\n    'Max-Forwards',\n    'Origin',\n    'Pragma',\n    'Proxy-Authorization',\n    'Range',\n    'Referer',\n    'TE',\n    'Upgrade',\n    'User-Agent',\n    'Via',\n    'Warning',\n    'X-Requested-With',\n    'DNT',\n    'X-Forwarded-For',\n    'X-Forwarded-Host',\n    'X-Forwarded-Proto',\n    'Front-End-Https',\n    'X-Http-Method-Override',\n    'X-ATT-DeviceId',\n    'X-Wap-Profile',\n    'Proxy-Connection',\n    'X-UIDH',\n    'X-Csrf-Token',\n    'X-Request-ID',\n    'X-Correlation-ID',\n    'Save-Data'\n  ];\n\n  static const Map<String, List<String>> commonHeaderValues = {\n    'Accept': [\n      'application/json, text/plain, */*',\n      'application/xml, text/xml, */*',\n      'text/html, application/xhtml+xml, */*',\n      '*/*'\n    ],\n    'Accept-Charset': ['utf-8, iso-8859-1;q=0.5', 'utf-8'],\n    'Accept-Encoding': ['gzip, deflate, br', 'gzip, deflate'],\n    'Accept-Language': ['en-US,en;q=0.9', 'zh-CN,zh;q=0.9'],\n    'Cache-Control': ['no-cache', 'max-age=0', 'no-store'],\n    'Connection': ['keep-alive', 'close'],\n    'Content-Type': [\n      'application/json',\n      'application/x-www-form-urlencoded',\n      'multipart/form-data',\n      'text/plain',\n      'text/html',\n      'application/xml'\n    ],\n    'User-Agent': [\n      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',\n      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',\n      'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1',\n      'Mozilla/5.0 (Android 11; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0'\n    ],\n  };\n\n  final LinkedHashMap<String, List<String>> _headers = LinkedHashMap<String, List<String>>();\n\n  // 由小写标头名称键入的原始标头名称。\n  final Map<String, String> _originalHeaderNames = {};\n\n  HttpHeaders();\n\n  ///设置header。\n  void set(String name, String value) {\n    var original = _originalHeaderNames[name.toLowerCase()];\n    if (original != null && original != name) {\n      _headers.remove(original);\n    }\n\n    _headers[name] = [value];\n    _originalHeaderNames[name.toLowerCase()] = name;\n  }\n\n  ///添加header。\n  void add(String name, String value) {\n    addValues(name, [value]);\n  }\n\n  ///添加header。\n  void addValues(String name, List<String> values) {\n    var original = _originalHeaderNames[name.toLowerCase()];\n    if (original != null && original != name) {\n      var old = _headers.remove(original);\n      _headers[name] = List.from(old!);\n    }\n    if (_headers[name] == null) {\n      _headers[name] = [];\n    }\n\n    _headers[name]?.addAll(values);\n    _originalHeaderNames[name.toLowerCase()] = name;\n  }\n\n  ///从headers中添加\n  addAll(HttpHeaders? headers) {\n    headers?.forEach((key, values) {\n      for (var val in values) {\n        add(key, val);\n      }\n    });\n  }\n\n  String? get(String name) {\n    return getList(name)?.first;\n  }\n\n  String getOriginalName(String name) {\n    return _originalHeaderNames[name.toLowerCase()] ?? name;\n  }\n\n  List<String>? getList(String name) {\n    var originalHeaderName = _originalHeaderNames[name.toLowerCase()];\n    if (originalHeaderName == null) {\n      return null;\n    }\n    return _headers[originalHeaderName];\n  }\n\n  bool remove(String name) {\n    var originalHeaderName = _originalHeaderNames.remove(name.toLowerCase());\n    _headers.remove(originalHeaderName);\n    return originalHeaderName != null;\n  }\n\n  int? getInt(String name) {\n    final value = get(name);\n    if (value == null) {\n      return null;\n    }\n    return int.parse(value);\n  }\n\n  bool getBool(String name) {\n    final value = get(name);\n    if (value == null) {\n      return false;\n    }\n    return value.toLowerCase() == \"true\";\n  }\n\n  int get contentLength => getInt(CONTENT_LENGTH) ?? 0;\n\n  set contentLength(int contentLength) => set(CONTENT_LENGTH, contentLength.toString());\n\n  String? get contentEncoding => get(HttpHeaders.CONTENT_ENCODING)?.toLowerCase();\n\n  bool get isGzip => contentEncoding == \"gzip\";\n\n  bool get isChunked => get(HttpHeaders.TRANSFER_ENCODING)?.toLowerCase().trimLeft() == \"chunked\";\n\n  List<String> get cookies => getList(Cookie) ?? [];\n\n  void forEach(void Function(String name, List<String> values) f) {\n    _headers.forEach(f);\n  }\n\n  Iterable<MapEntry<String, List<String>>> get entries => _headers.entries;\n\n  set contentType(String contentType) => set(CONTENT_TYPE, contentType);\n\n  String get contentType => get(CONTENT_TYPE) ?? \"\";\n\n  String? get host => get(HOST);\n\n  set host(String? host) {\n    if (host != null) {\n      set(HOST, host);\n    }\n  }\n\n  //清空\n  void clear() {\n    _headers.clear();\n    _originalHeaderNames.clear();\n  }\n\n  String headerLines() {\n    StringBuffer sb = StringBuffer();\n    forEach((name, values) {\n      for (var value in values) {\n        sb.writeln(\"$name: $value\");\n      }\n    });\n\n    return sb.toString();\n  }\n\n  ///转换json\n  Map<String, dynamic> toJson() {\n    Map<String, dynamic> json = {};\n    forEach((name, values) {\n      json[name] = values;\n    });\n    return json;\n  }\n\n  ///转换json\n  Map<String, String> toMap() {\n    Map<String, String> json = {};\n    forEach((name, values) {\n      json[name] = values.join(\";\");\n    });\n    return json;\n  }\n\n  Map<String, List<String>> getHeaders() {\n    return _headers;\n  }\n\n  ///从json解析\n  factory HttpHeaders.fromJson(Map<String, dynamic> json) {\n    HttpHeaders headers = HttpHeaders();\n    json.forEach((key, values) {\n      for (var element in (values as List)) {\n        headers.add(key, element.toString());\n      }\n    });\n\n    return headers;\n  }\n\n  ///原始header文本\n  String toRawHeaders() {\n    StringBuffer sb = StringBuffer();\n    forEach((name, values) {\n      for (var value in values) {\n        sb.writeln(\"$name: $value\");\n      }\n    });\n\n    return sb.toString();\n  }\n\n  @override\n  String toString() {\n    return 'HttpHeaders{$_headers}';\n  }\n}\n"
  },
  {
    "path": "lib/network/http/parse/body_reader.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:math';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/network/http/constants.dart';\nimport 'package:proxypin/network/http/http.dart';\n\nimport '../../../utils/num.dart';\nimport '../codec.dart';\n\nclass Result {\n  final bool isDone;\n  final bool supportedParse;\n\n  Uint8List? body;\n\n  Result(this.isDone, {this.body, this.supportedParse = true});\n}\n\nclass BodyReader {\n  final HttpMessage message;\n\n  // BytesBuilder msgBytes = BytesBuilder();\n  int _offset = 0;\n  ReaderState _state;\n\n  final BytesBuilder _bodyBuffer = BytesBuilder();\n\n  ///chunked编码 剩余未读取的chunk大小\n  int _chunkReadableSize = 0;\n\n  BodyReader(this.message)\n      : _state = message.headers.isChunked ? ReaderState.readChunkSize : ReaderState.readFixedLengthContent;\n\n  Result readBody(Uint8List data) {\n    if (_bodyBuffer.length > Codec.maxBodyLength) {\n      _bodyBuffer.clear();\n      throw ParserException('Body length exceeds ${Codec.maxBodyLength}');\n    }\n\n    _offset = 0;\n\n    if (message.headers.contentType == 'video/x-flv' || message.headers.contentType.startsWith(\"text/event-stream\")) {\n      //Directly forward without processing for now\n      return Result(false, supportedParse: false, body: data);\n    }\n\n    //chunked编码\n    if (message.headers.isChunked) {\n      _readChunked(data);\n    } else {\n      _readFixedLengthContent(data);\n    }\n\n    if (_state == ReaderState.done) {\n      var body = _bodyBuffer.toBytes();\n      _bodyBuffer.clear();\n      return Result(true, body: body);\n    }\n\n    return Result(false);\n  }\n\n  void _readFixedLengthContent(Uint8List data) {\n    if (message.contentLength > 0) {\n      _bodyBuffer.add(data.sublist(_offset));\n    }\n\n    if (message.contentLength == -1 || _bodyBuffer.length >= message.contentLength) {\n      _state = ReaderState.done;\n    }\n  }\n\n  void _readChunked(Uint8List data) {\n    while (_offset < data.length) {\n      //读取chunk length\n      if (_state == ReaderState.readChunkSize) {\n        _chunkReadableSize = _readChunkSize(data);\n\n        if (_chunkReadableSize == 0) {\n          //chunked编码结束\n          _state = ReaderState.done;\n          break;\n        }\n\n        if (_chunkReadableSize == -1) {\n          continue;\n        }\n        _state = ReaderState.readChunkedContent;\n      }\n\n      //读取chunk内容\n      if (_state == ReaderState.readChunkedContent) {\n        int end = min(data.length, _offset + _chunkReadableSize);\n        _bodyBuffer.add(data.sublist(_offset, end));\n\n        //可读大小\n        _chunkReadableSize -= (end - _offset);\n        _offset = end;\n        if (_chunkReadableSize == 0) {\n          _state = ReaderState.readChunkSize;\n          _offset += 2; //内容结尾\\r\\n\n        }\n      }\n    }\n  }\n\n  int _readChunkSize(Uint8List data) {\n    if (_offset >= data.length) {\n      return -1;\n    }\n\n    for (int i = _offset; i < data.length; i++) {\n      /// chunked编码内容结尾\\r\\n\n      if (data[i] == HttpConstants.lf) {\n        if (i > 0 && data[i - 1] == HttpConstants.cr) {\n          var line = data.sublist(_offset, i - 1);\n          _offset = i + 1;\n          if (line.isEmpty) {\n            return -1;\n          }\n          return hexToInt(String.fromCharCodes(line));\n        }\n\n        //可能上个包是结尾\\r 最好做法是缓存上个不完整的包，先临时处理下\n        if (data.length == 1) {\n          _offset = i + 1;\n          return -1;\n        }\n      }\n    }\n\n    throw Exception('Invalid chunked encoding line: ${String.fromCharCodes(data)}');\n  }\n}\n\nenum ReaderState { readFixedLengthContent, readChunked, readChunkSize, readChunkedContent, done }\n"
  },
  {
    "path": "lib/network/http/parse/http_parser.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:proxypin/network/http/constants.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\nimport 'package:proxypin/network/util/byte_buf.dart';\n\n/// http解析器\nclass HttpParse {\n  static const int defaultMaxLength = 102400;\n\n  /// 解析请求行\n  List<String> parseInitialLine(ByteBuf data, int size) {\n    List<String> initialLine = [];\n    var startIndex = data.readerIndex;\n    for (int i = data.readerIndex; i < size; i++) {\n      if (_isLineEnd(data, i)) {\n        //请求行结束\n        Uint8List requestLine = data.readBytes(i - data.readerIndex);\n        data.skipBytes(2);\n        initialLine = _splitLine(requestLine);\n        break;\n      }\n    }\n\n    if (initialLine.length == 3) {\n      return initialLine;\n    }\n\n    if (data.length > defaultMaxLength) {\n      throw Exception(\"request line too long\");\n    }\n\n    data.readerIndex = startIndex;\n    return [];\n  }\n\n  //分割行\n  List<String> _splitLine(Uint8List data) {\n    List<String> lines = [];\n    int start = 0;\n    for (int i = 0; i < data.length; i++) {\n      if (data[i] == HttpConstants.sp) {\n        lines.add(String.fromCharCodes(data.sublist(start, i)));\n        start = i + 1;\n        if (lines.length == 2) {\n          break;\n        }\n      }\n    }\n    lines.add(String.fromCharCodes(data.sublist(start)));\n    return lines;\n  }\n\n  /// 解析请求头\n  bool parseHeaders(ByteBuf data, HttpHeaders headers) {\n    if (!data.isReadable()) {\n      return false;\n    }\n\n    int startIndex = data.readerIndex;\n    for (int i = data.readerIndex; i < data.length; i++) {\n      if ((i - startIndex) > defaultMaxLength) {\n        throw Exception(\"header too long\");\n      }\n\n      if (_isLineEnd(data, i)) {\n        Uint8List line = data.readBytes(i - data.readerIndex);\n        data.skipBytes(2);\n        if (line.isEmpty) {\n          break;\n        }\n        var header = _splitHeader(line);\n        headers.add(header[0], header[1]);\n      }\n    }\n\n    //\\r\\n \\r\\n结束\n    return _isLineEnd(data, data.readerIndex - 4) && _isLineEnd(data, data.readerIndex - 2);\n  }\n\n  //是否行结束\n  bool _isLineEnd(ByteBuf data, int index) {\n    return index + 1 < data.length && data.get(index) == HttpConstants.cr && data.get(index + 1) == HttpConstants.lf;\n  }\n\n  //分割头\n  List<String> _splitHeader(List<int> data) {\n    List<String> headers = [];\n    for (int i = 0; i < data.length; i++) {\n      if (data[i] == HttpConstants.colon) {\n        headers.add(String.fromCharCodes(data.sublist(0, i)));\n\n        if (data[i + 1] == HttpConstants.sp) {\n          headers.add(String.fromCharCodes(data.sublist(i + 2)));\n        } else {\n          headers.add(String.fromCharCodes(data.sublist(i + 1)));\n        }\n        break;\n      }\n    }\n    return headers;\n  }\n}\n"
  },
  {
    "path": "lib/network/http/sse.dart",
    "content": "/*\n * Server-Sent Events (text/event-stream) incremental decoder\n */\n\nimport 'dart:convert';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/network/http/websocket.dart';\n\n/// Parse SSE stream chunks into message frames.\n/// We reuse WebSocketFrame as a generic message container so UI and listeners work.\nclass SseDecoder {\n  final StringBuffer _lineBuf = StringBuffer();\n\n  // current event fields\n  final StringBuffer _data = StringBuffer();\n  String? _event;\n  String? _id;\n  int? _retry;\n\n  /// Feed a chunk of bytes and return zero or more frames assembled.\n  List<WebSocketFrame> feed(Uint8List bytes) {\n    final List<WebSocketFrame> frames = [];\n\n    // Append decoded text to buffer; allowMalformed to survive split UTF-8 sequences.\n    _lineBuf.write(utf8.decode(bytes, allowMalformed: true));\n\n    while (true) {\n      final String current = _lineBuf.toString();\n      final int nl = current.indexOf('\\n');\n      if (nl == -1) break;\n\n      String line = current.substring(0, nl);\n      _lineBuf.clear();\n      if (nl + 1 < current.length) _lineBuf.write(current.substring(nl + 1));\n\n      if (line.endsWith('\\r')) line = line.substring(0, line.length - 1);\n\n      if (line.isEmpty) {\n        // End of event: emit if any data collected\n        if (_data.isNotEmpty) {\n          String dataValue = _data.toString();\n          if (dataValue.endsWith('\\n')) dataValue = dataValue.substring(0, dataValue.length - 1);\n\n          // Build a text frame from the SSE event. Include event/id headers if present as a prefix comment.\n          final String payloadText = _event == null && _id == null\n              ? dataValue\n              : _buildLabeledPayload(dataValue, event: _event, id: _id, retry: _retry);\n\n          frames.add(_textFrame(payloadText));\n        }\n        _resetEventState();\n        continue;\n      }\n\n      if (line.startsWith(':')) {\n        // comment line – ignore\n        continue;\n      }\n\n      final int colon = line.indexOf(':');\n      final String field = (colon == -1) ? line : line.substring(0, colon);\n      String value = (colon == -1) ? '' : line.substring(colon + 1);\n      if (value.startsWith(' ')) value = value.substring(1);\n\n      switch (field) {\n        case 'data':\n          _data.write(value);\n          _data.write('\\n');\n          break;\n        case 'event':\n          _event = value;\n          break;\n        case 'id':\n          _id = value;\n          break;\n        case 'retry':\n          _retry = int.tryParse(value);\n          break;\n        default:\n          // ignore unknown fields\n          break;\n      }\n    }\n\n    return frames;\n  }\n\n  void _resetEventState() {\n    _data.clear();\n    _event = null;\n    _id = null;\n    _retry = null;\n  }\n\n  String _buildLabeledPayload(String data, {String? event, String? id, int? retry}) {\n    final StringBuffer b = StringBuffer();\n    if (event != null && event.isNotEmpty) b.writeln('event: $event');\n    if (id != null && id.isNotEmpty) b.writeln('id: $id');\n    if (retry != null) b.writeln('retry: $retry');\n    b.write(data);\n    return b.toString();\n  }\n\n  WebSocketFrame _textFrame(String text) {\n    final bytes = utf8.encode(text);\n    return WebSocketFrame(\n      fin: true,\n      opcode: 0x01, // text\n      mask: false,\n      payloadLength: bytes.length,\n      maskingKey: 0,\n      payloadData: Uint8List.fromList(bytes),\n    );\n  }\n}\n\n"
  },
  {
    "path": "lib/network/http/websocket.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/network/util/logger.dart';\n\nclass WebSocketFrame {\n  final bool fin;\n\n  /*\n      0x00 denotes a continuation frame\n      0x01 表示一个text frame\n      0x02 表示一个binary frame\n      0x03 ~~ 0x07 are reserved for further non-control frames,为将来的非控制消息片段保留测操作码\n      0x08 表示连接关闭\n      0x09 表示 ping (心跳检测相关)\n      0x0a 表示 pong (心跳检测相关)\n   */\n  final int opcode; //4bit\n  final bool mask; //1bit\n  final int maskingKey;\n\n  final int payloadLength;\n  final Uint8List payloadData;\n\n  bool isFromClient = false;\n  DateTime time;\n\n  WebSocketFrame({\n    required this.fin,\n    required this.opcode,\n    required this.mask,\n    required this.payloadLength,\n    required this.maskingKey,\n    required this.payloadData,\n    DateTime? time,\n  }) : time = time ?? DateTime.now();\n\n  bool get isText => opcode == 0x01;\n\n  bool get isBinary => opcode == 0x02;\n\n  String get payloadDataAsString {\n    if (opcode == 0x08) {\n      return '连接关闭';\n    }\n    if (opcode == 0x02) {\n      return '二进制数据';\n    }\n    try {\n      return utf8.decode(payloadData);\n    } catch (e) {\n      return String.fromCharCodes(payloadData);\n    }\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'fin': fin,\n      'opcode': opcode,\n      'mask': mask,\n      'maskingKey': maskingKey,\n      'payloadLength': payloadLength,\n      // use base64 to avoid binary corruption in JSON\n      'payloadData': base64Encode(payloadData),\n      'isFromClient': isFromClient,\n      'time': time.millisecondsSinceEpoch,\n    };\n  }\n\n  factory WebSocketFrame.fromJson(Map<String, dynamic> json) {\n    final payload = base64Decode(json['payloadData']?.toString() ?? '');\n    final frame = WebSocketFrame(\n      fin: json['fin'] == true,\n      opcode: (json['opcode'] ?? 0) as int,\n      mask: json['mask'] == true,\n      payloadLength: (json['payloadLength'] ?? payload.length) as int,\n      maskingKey: (json['maskingKey'] ?? 0) as int,\n      payloadData: Uint8List.fromList(payload),\n      time: json['time'] == null\n          ? DateTime.now()\n          : DateTime.fromMillisecondsSinceEpoch((json['time'] as num).toInt()),\n    );\n    frame.isFromClient = json['isFromClient'] == true;\n    return frame;\n  }\n}\n\n///websocket 解码器\nclass WebSocketDecoder {\n  ByteBuffer buffer = ByteBuffer();\n\n  WebSocketFrame? decode(Uint8List newData) {\n    buffer.putBytes(newData);\n    if (!canParseWebSocketFrame(buffer.bytes)) {\n      return null;\n    }\n\n    try {\n      WebSocketFrame frame = _parseWebSocketFrame(buffer.bytes);\n      buffer.clear();\n      return frame;\n    } catch (e, stackTrace) {\n      logger.e(\"WebSocket decode error\", error: e, stackTrace: stackTrace);\n      return null;\n    }\n  }\n\n  bool canParseWebSocketFrame(Uint8List data) {\n    if (data.length < 2) {\n      return false;\n    }\n\n    var reader = ByteData.sublistView(data);\n\n    var opcode = reader.getUint8(0) & 0x0f;\n    if (opcode > 0xA) {\n      return false;\n    }\n\n    var mask = reader.getUint8(1) >> 7;\n    int payloadStart = 2;\n    int payloadLength = reader.getUint8(1) & 0x7f;\n\n    if (payloadLength == 126) {\n      if (data.length < 4) return false;\n      payloadLength = reader.getUint16(2);\n      payloadStart += 2;\n    } else if (payloadLength == 127) {\n      if (data.length < 10) return false;\n      payloadLength = reader.getUint64(2);\n      payloadStart += 8;\n    }\n\n    if (mask == 1) {\n      if (data.length < payloadStart + 4) {\n        return false;\n      }\n      payloadStart += 4;\n    }\n\n    if (data.length < payloadStart + payloadLength) {\n      return false;\n    }\n\n    return true;\n  }\n\n  WebSocketFrame _parseWebSocketFrame(Uint8List data) {\n    var reader = ByteData.sublistView(data);\n\n    var fin = reader.getUint8(0) >> 7;\n    //解析 rsv1\n    var rsv1 = (reader.getUint8(0) >> 6) & 0x01;\n\n    var opcode = reader.getUint8(0) & 0x0f;\n\n    var mask = reader.getUint8(1) >> 7;\n    int payloadLength = reader.getUint8(1) & 0x7f;\n\n    int payloadStart = 2;\n\n    if (payloadLength == 126) {\n      payloadLength = reader.getUint16(2);\n      payloadStart += 2;\n    } else if (payloadLength == 127) {\n      payloadLength = reader.getUint64(2);\n      payloadStart += 8;\n    }\n\n    var maskingKey = 0;\n    if (mask == 1) {\n      maskingKey = reader.getUint32(payloadStart);\n      payloadStart += 4;\n    }\n\n    int payloadDataLength = payloadLength;\n    if (payloadStart + payloadDataLength > data.length) {\n      payloadDataLength = data.length - payloadStart;\n      logger.w(\"Payload data length exceeds available data, truncating.\");\n    }\n\n    var payloadData = data.sublist(payloadStart, payloadStart + payloadDataLength);\n\n    if (mask == 1) {\n      payloadData = unmaskPayload(payloadData, maskingKey);\n    }\n\n    if (rsv1 == 1) {\n      //inflate\n      payloadData = decompress(payloadData);\n    }\n\n    return WebSocketFrame(\n      fin: fin == 1,\n      opcode: opcode,\n      mask: mask == 1,\n      maskingKey: maskingKey,\n      payloadLength: payloadLength,\n      payloadData: payloadData,\n    );\n  }\n\n  ZLibDecoder? _decoder;\n\n  ZLibDecoder _ensureDecoder() => _decoder ?? ZLibDecoder(raw: true);\n\n  Uint8List decompress(Uint8List msg) {\n    try {\n      return Uint8List.fromList(_ensureDecoder().convert(msg));\n    } catch (e) {\n      logger.e(\"Decompression error\", error: e);\n      return msg;\n    }\n  }\n\n  Uint8List unmaskPayload(Uint8List payloadData, int maskingKey) {\n    var unmaskedData = Uint8List(payloadData.length);\n    for (var i = 0; i < payloadData.length; i++) {\n      var keyByte = (maskingKey >> ((3 - (i % 4)) * 8)) & 0xff;\n      unmaskedData[i] = payloadData[i] ^ keyByte;\n    }\n    return unmaskedData;\n  }\n}\n\nclass ByteBuffer {\n  Uint8List _bytes = Uint8List(0);\n\n  Uint8List get bytes => _bytes;\n\n  void putBytes(Uint8List newBytes) {\n    Uint8List tmp = Uint8List(_bytes.length + newBytes.length);\n    tmp.setAll(0, _bytes);\n    tmp.setAll(_bytes.length, newBytes);\n    _bytes = tmp;\n  }\n\n  void clear() {\n    _bytes = Uint8List(0);\n  }\n}\n"
  },
  {
    "path": "lib/network/socks/socks5.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/http/codec.dart';\nimport 'package:proxypin/network/util/attribute_keys.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nimport '../channel/host_port.dart';\n\n/// @author wanghongen\nclass Socks5 {\n  static const int version = 5;\n  static const int methodNoAuth = 0;\n  static const int methodNoAcceptable = 0xff;\n\n  static const int cmdConnect = 1;\n\n  static const int atypIpv4 = 1;\n  static const int atypDomain = 3;\n  static const int atypIpv6 = 4;\n\n  static const int repSuccess = 0;\n  static const int repCommandNotSupported = 7;\n  static const int repAddressTypeNotSupported = 8;\n\n  static const int repSocks5ServerAtypIpv4 = 0x01;\n  static const int repSocks5ServerAtypDomain = 0x03;\n  static const int repSocks5ServerAtypIpv6 = 0x04;\n\n  static bool isSocks5(Uint8List data) {\n    return (data.length == 3 || data.length == 4) &&\n        data[0] == version &&\n        (data[1] == 1 || data[1] == 2) &&\n        data[2] == methodNoAuth;\n  }\n}\n\n///Detects the version of the current SOCKS connection and initializes the pipeline with Socks5InitialRequestDecoder.\nclass SocksServerHandler extends ChannelHandler<Uint8List> {\n  late Decoder originalDecoder;\n  late Encoder originalEncoder;\n  final ChannelHandler originalHandler;\n\n  SocksState socksState = SocksState.init;\n\n  SocksServerHandler(this.originalDecoder, this.originalEncoder, this.originalHandler);\n\n  @override\n  Future<void> channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async {\n    int idx = 0;\n    final int version = msg[idx++];\n    if (version != Socks5.version) {\n      await channel.writeBytes(Uint8List.fromList([Socks5.version, Socks5.methodNoAcceptable]));\n      channel.dispatcher.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS version: $version'));\n      return;\n    }\n\n    if (socksState == SocksState.init) {\n      //no auth\n      await channel.writeBytes(Uint8List.fromList([Socks5.version, Socks5.methodNoAuth]));\n      socksState = SocksState.connect;\n      return;\n    }\n\n    if (socksState == SocksState.connect) {\n      final int cmd = msg[idx++];\n      if (cmd != Socks5.cmdConnect) {\n        var out = encodeCommandResponse(Socks5.repCommandNotSupported);\n        await channel.writeBytes(out);\n        channel.dispatcher.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS cmd: $cmd'));\n        return;\n      }\n\n      //skip RSV\n      idx++;\n\n      final int dstAddrType = msg[idx++];\n      String host;\n\n      if (dstAddrType == Socks5.atypIpv4) {\n        host = '${msg[idx++]}.${msg[idx++]}.${msg[idx++]}.${msg[idx++]}';\n      } else if (dstAddrType == Socks5.atypDomain) {\n        int len = msg[idx++];\n        host = utf8.decode(msg.sublist(idx, idx + len));\n        idx += len;\n      } else if (dstAddrType == Socks5.atypIpv6) {\n        List<String> parts = [];\n        for (int i = 0; i < 8; i++) {\n          int part = msg[idx++] << 8 | msg[idx++];\n          parts.add(part.toRadixString(16));\n        }\n        host = parts.join(':');\n      } else {\n        var out = encodeCommandResponse(Socks5.repAddressTypeNotSupported);\n        await channel.writeBytes(out);\n        channel.dispatcher.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS atyp: $dstAddrType'));\n        return;\n      }\n\n      final int port = msg[idx++] << 8 | msg[idx++];\n      final proxyInfo = ProxyInfo.of(host, port);\n\n      logger.d('[${channel.id}] Socks5 connect ${proxyInfo.host}:${proxyInfo.port}');\n      channelContext.putAttribute(AttributeKeys.socks5Proxy, proxyInfo);\n\n      final out = encodeCommandResponse(Socks5.repSuccess, bndAddrType: Socks5.repSocks5ServerAtypIpv4);\n      await channel.writeBytes(out);\n\n      channel.dispatcher.handle(originalDecoder, originalEncoder, originalHandler);\n      socksState = SocksState.connected;\n      return;\n    }\n  }\n\n  Uint8List encodeCommandResponse(int status, {int bndAddrType = 0, String? bndAddr, int bndPort = 0}) {\n    var out = BytesBuilder();\n    out.addByte(Socks5.version);\n    out.addByte(status);\n    out.addByte(0x00); //RSV\n    out.addByte(bndAddrType);\n\n    if (bndAddr != null) {\n      out.add(Int8List.fromList(bndAddr.split('.').map((e) => int.parse(e)).toList()));\n    } else {\n      out.add(Int8List.fromList([0, 0, 0, 0]));\n    }\n    out.addByte(bndPort >> 8);\n    out.addByte(bndPort & 0xff);\n    return out.takeBytes();\n  }\n}\n\nenum SocksState {\n  init,\n  auth,\n  connect,\n  connected,\n}\n"
  },
  {
    "path": "lib/network/util/attribute_keys.dart",
    "content": "/// @author wanghongen\n/// 2023/5/23\ninterface class AttributeKeys {\n  static const String host = \"HOST\";\n  static const String domain = \"DOMAIN\";\n  static const String uri = \"URI\";\n  static const String request = \"REQUEST\";\n  static const String remote = \"REMOTE\";\n  static const String proxyInfo = \"PROXY_INFO\";\n  static const String socks5Proxy = \"SOCKS5_PROXY\";\n  static const String processInfo = \"PROCESS_INFO\";\n}\n"
  },
  {
    "path": "lib/network/util/byte_buf.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:typed_data';\n\n///类似于netty ByteBuf\n\nclass ByteBuf {\n  late Uint8List _buffer;\n  int readerIndex = 0;\n  int writerIndex = 0;\n\n  ByteBuf([List<int>? bytes]) {\n    if (bytes != null) {\n      _buffer = Uint8List.fromList(bytes);\n      writerIndex = bytes.length;\n    } else {\n      _buffer = Uint8List(0); // Initial buffer size\n    }\n  }\n\n  int get length => writerIndex;\n\n  Uint8List get bytes => Uint8List.sublistView(_buffer, 0, writerIndex);\n\n  void add(List<int> bytes) {\n    _ensureCapacity(writerIndex + bytes.length);\n    _buffer.setRange(writerIndex, writerIndex + bytes.length, bytes);\n    writerIndex += bytes.length;\n  }\n\n  void clear() {\n    readerIndex = 0;\n    writerIndex = 0;\n    _buffer = Uint8List(0);\n  }\n\n  ///释放已读的空间\n  void clearRead() {\n    if (readerIndex == writerIndex) {\n      clear();\n      return;\n    }\n\n    if (readerIndex > 0) {\n      _buffer = Uint8List.sublistView(_buffer, readerIndex, writerIndex);\n      writerIndex -= readerIndex;\n      readerIndex = 0;\n    }\n  }\n\n  bool isReadable() => readerIndex < writerIndex;\n\n  int readableBytes() => writerIndex - readerIndex;\n\n  Uint8List readAvailableBytes() => readBytes(readableBytes());\n\n  Uint8List readBytes(int length) {\n    Uint8List result = Uint8List.sublistView(_buffer, readerIndex, readerIndex + length);\n    readerIndex += length;\n    return result;\n  }\n\n  void skipBytes(int length) {\n    readerIndex += length;\n  }\n\n  int read() => _buffer[readerIndex++];\n\n  int readByte() => _buffer[readerIndex++];\n\n  int readShort() {\n    int value = (_buffer[readerIndex] << 8) | _buffer[readerIndex + 1];\n    readerIndex += 2;\n    return value;\n  }\n\n  int readInt() {\n    int value = (_buffer[readerIndex] << 24) |\n        (_buffer[readerIndex + 1] << 16) |\n        (_buffer[readerIndex + 2] << 8) |\n        _buffer[readerIndex + 3];\n    readerIndex += 4;\n    return value;\n  }\n\n  int get(int index) => _buffer[index];\n\n  void truncate(int len) {\n    if (len > readableBytes()) {\n      throw Exception(\"Insufficient data\");\n    }\n\n    writerIndex = readerIndex + len;\n  }\n\n  ByteBuf dup() {\n    ByteBuf buf = ByteBuf();\n    buf._buffer = Uint8List.fromList(_buffer);\n    buf.readerIndex = readerIndex;\n    buf.writerIndex = writerIndex;\n    return buf;\n  }\n\n  void _ensureCapacity(int required) {\n    if (_buffer.length < required) {\n      int newSize = _buffer.length <= 1 ? required : _buffer.length * 2;\n      while (newSize < required) {\n        newSize *= 2;\n      }\n      Uint8List newBuffer = Uint8List(newSize);\n      newBuffer.setRange(0, writerIndex, _buffer);\n      _buffer = newBuffer;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/network/util/byte_utils.dart",
    "content": "import 'dart:typed_data';\n\nList<int> viewOrSublist(List<int> data, int offset, int length) {\n  if (data is Uint8List) {\n    return Uint8List.view(data.buffer, data.offsetInBytes + offset, length);\n  } else {\n    return data.sublist(offset, offset + length);\n  }\n}\n\nint readInt64(List<int> bytes, int offset) {\n  var high = readInt32(bytes, offset);\n  var low = readInt32(bytes, offset + 4);\n  return high << 32 | low;\n}\n\nint readInt32(List<int> bytes, int offset) {\n  return (bytes[offset] << 24) |\n      (bytes[offset + 1] << 16) |\n      (bytes[offset + 2] << 8) |\n      bytes[offset + 3];\n}\n\nint readInt24(List<int> bytes, int offset) {\n  return (bytes[offset] << 16) | (bytes[offset + 1] << 8) | bytes[offset + 2];\n}\n\nint readInt16(List<int> bytes, int offset) {\n  return (bytes[offset] << 8) | bytes[offset + 1];\n}\n\nvoid setInt64(List<int> bytes, int offset, int value) {\n  setInt32(bytes, offset, value >> 32);\n  setInt32(bytes, offset + 4, value & 0xffffffff);\n}\n\nvoid setInt32(List<int> bytes, int offset, int value) {\n  bytes[offset] = (value >> 24) & 0xff;\n  bytes[offset + 1] = (value >> 16) & 0xff;\n  bytes[offset + 2] = (value >> 8) & 0xff;\n  bytes[offset + 3] = value & 0xff;\n}\n\nvoid setInt24(List<int> bytes, int offset, int value) {\n  bytes[offset] = (value >> 16) & 0xff;\n  bytes[offset + 1] = (value >> 8) & 0xff;\n  bytes[offset + 2] = value & 0xff;\n}\n\nvoid setInt16(List<int> bytes, int offset, int value) {\n  bytes[offset] = (value >> 8) & 0xff;\n  bytes[offset + 1] = value & 0xff;\n}\n"
  },
  {
    "path": "lib/network/util/cache.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:async';\nimport 'dart:collection';\n\n/// A cache that expires entries after a given duration.\n/// The cache uses a timer to remove entries after the specified duration.\n/// @author WangHongEn\nclass ExpiringCache<K, V> {\n  final Duration duration;\n  final _cache = <K, V>{};\n  final _expirationTimes = <K, Timer>{};\n\n  ExpiringCache(this.duration);\n\n  void set(K key, V value) {\n    _expirationTimes[key]?.cancel();\n    _cache[key] = value;\n    _expirationTimes[key] = Timer(duration, () => remove(key));\n  }\n\n  void operator []=(K key, V value) => set(key, value);\n\n  V? putIfAbsent(K key, V Function() ifAbsent) {\n    if (_cache.containsKey(key)) {\n      return _cache[key];\n    }\n    final value = ifAbsent();\n    set(key, value);\n    return value;\n  }\n\n  bool containsKey(K key) {\n    return _cache.containsKey(key);\n  }\n\n  V? get(K key) {\n    return _cache[key];\n  }\n\n  V? operator [](K key) => get(key);\n\n  V? remove(K key) {\n    _expirationTimes[key]?.cancel();\n    _expirationTimes.remove(key);\n    return _cache.remove(key);\n  }\n\n  void clear() {\n    for (var timer in _expirationTimes.values) {\n      timer.cancel();\n    }\n    _expirationTimes.clear();\n    _cache.clear();\n  }\n}\n\nclass LruCache<K, V> {\n  final int capacity;\n  final _cache = LinkedHashMap<K, V>();\n\n  LruCache(this.capacity);\n\n  V? get(K key) {\n    if (!_cache.containsKey(key)) {\n      return null;\n    }\n\n    // Move the accessed key to the end to show that it was recently used\n    final value = _cache.remove(key);\n    _cache[key] = value as V;\n    return value;\n  }\n\n  V pubIfAbsent(K key, V Function() ifAbsent) {\n    if (_cache.containsKey(key)) {\n      return _cache[key]!;\n    }\n\n    final value = ifAbsent();\n    set(key, value);\n    return value;\n  }\n\n  void set(K key, V value) {\n    if (_cache.containsKey(key)) {\n      // Remove the old value\n      _cache.remove(key);\n    } else if (_cache.length == capacity) {\n      // Remove the first key (least recently used)\n      _cache.remove(_cache.keys.first);\n    }\n    _cache[key] = value;\n  }\n\n  void remove(K key) {\n    _cache.remove(key);\n  }\n\n  int get length => _cache.length;\n\n  void clear() {\n    _cache.clear();\n  }\n}\n"
  },
  {
    "path": "lib/network/util/cert/basic_constraints.dart",
    "content": "/// @author wanghongen\n/// 2024/7/28\nclass BasicConstraints {\n  final bool isCA;\n  final int? pathLenConstraint;\n  final bool critical;\n\n  BasicConstraints({required this.isCA, this.pathLenConstraint, this.critical = true});\n}\n"
  },
  {
    "path": "lib/network/util/cert/cert_data.dart",
    "content": "import 'dart:typed_data';\n\nimport 'extension.dart';\nimport 'key_usage.dart';\n\nclass X509CertificateData {\n  /// The subject data of the certificate\n  Map<String, String?> subject;\n\n  /// The version of the certificate\n  int version;\n\n  BigInt serialNumber;\n\n  /// The signatureAlgorithm of the certificate\n  String signatureAlgorithm;\n\n  /// The readable name of the signatureAlgorithm of the certificate\n  String? signatureAlgorithmReadableName;\n\n  Map<String, String?> issuer;\n\n  /// The validity of the certificate\n  X509CertificateValidity validity;\n\n  /// The sha1 thumbprint for the certificate\n  String? sha1Thumbprint;\n\n  /// The sha256 thumbprint for the certificate\n  String? sha256Thumbprint;\n\n  /// The md5 thumbprint for the certificate\n  String? md5Thumbprint;\n\n  /// The public key data from the certificate\n  X509CertificatePublicKeyData publicKeyData;\n\n  /// The subject alternative names\n  List<String>? subjectAlternativNames;\n\n  /// The plain certificate pem string, that was used to decode.\n  String? plain;\n\n  /// The extended key usage extension\n  List<ExtendedKeyUsage>? extKeyUsage;\n\n  /// The certificate extensions\n  X509CertificateDataExtensions? extensions;\n\n  /// The signature\n  String? signature;\n\n  /// The tbsCertificateSeq as base64 string\n  String? tbsCertificateSeqAsString;\n\n  X509CertificateData({\n    required this.version,\n    required this.serialNumber,\n    required this.signatureAlgorithm,\n    required this.issuer,\n    required this.validity,\n    required this.subject,\n    // required this.tbsCertificate,\n    this.signatureAlgorithmReadableName,\n    this.sha1Thumbprint,\n    this.sha256Thumbprint,\n    this.md5Thumbprint,\n    required this.publicKeyData,\n    required this.subjectAlternativNames,\n    this.plain,\n    this.extKeyUsage,\n    this.extensions,\n    this.tbsCertificateSeqAsString,\n    required this.signature,\n  });\n}\n\nclass SubjectPublicKeyInfo {\n  /// The algorithm of the public key\n  String? algorithm;\n\n  /// The readable name of the algorithm\n  String? algorithmReadableName;\n\n  /// The parameter of the public key\n  String? parameter;\n\n  /// The readable name of the parameter\n  String? parameterReadableName;\n\n  /// The key length of the public key\n  int? length;\n\n  /// The sha1 thumbprint of the public key\n  String? sha1Thumbprint;\n\n  /// The sha256 thumbprint of the public key\n  String? sha256Thumbprint;\n\n  /// The bytes representing the public key as String\n  String? bytes;\n\n  /// The exponent used on a RSA public key\n  int? exponent;\n\n  SubjectPublicKeyInfo({\n    this.algorithm,\n    this.length,\n    this.sha1Thumbprint,\n    this.sha256Thumbprint,\n    this.bytes,\n    this.algorithmReadableName,\n    this.parameter,\n    this.parameterReadableName,\n    this.exponent,\n  });\n}\n\nclass X509CertificateValidity {\n  /// The start date\n  DateTime notBefore;\n\n  /// The end date\n  DateTime notAfter;\n\n  X509CertificateValidity({required this.notBefore, required this.notAfter});\n}\n\n//\n/// Model that represents the extensions of a x509Certificate\n///\nclass X509CertificateDataExtensions {\n  /// The subject alternative names\n  List<String>? subjectAlternativNames;\n\n  /// The extended key usage extension\n  List<ExtendedKeyUsage>? extKeyUsage;\n\n  /// The key usage extension\n  List<KeyUsage>? keyUsage;\n\n  /// The cA field of the basic constraints extension\n  bool? cA;\n\n  /// The pathLenConstraint field of the basic constraints extension\n  int? pathLenConstraint;\n\n  /// The base64 encoded VMC logo\n  VmcData? vmc;\n\n  /// The distribution points for the crl files. Normally a url.\n  List<String>? cRLDistributionPoints;\n\n  X509CertificateDataExtensions({\n    this.subjectAlternativNames,\n    this.extKeyUsage,\n    this.keyUsage,\n    this.cA,\n    this.pathLenConstraint,\n    this.vmc,\n    this.cRLDistributionPoints,\n  });\n}\n\n///\n/// Model that a public key from a X509Certificate\n///\nclass X509CertificatePublicKeyData {\n  /// The algorithm of the public key\n  String? algorithm;\n\n  /// The readable name of the algorithm\n  String? algorithmReadableName;\n\n  /// The parameter of the public key\n  String? parameter;\n\n  /// The readable name of the parameter\n  String? parameterReadableName;\n\n  /// The key length of the public key\n  int? length;\n\n  /// The sha1 thumbprint of the public key\n  String? sha1Thumbprint;\n\n  /// The sha256 thumbprint of the public key\n  String? sha256Thumbprint;\n\n  /// The bytes representing the public key as String\n  String? bytes;\n\n  Uint8List? plainSha1;\n\n  /// The exponent used on a RSA public key\n  int? exponent;\n\n  X509CertificatePublicKeyData({\n    this.algorithm,\n    this.length,\n    this.sha1Thumbprint,\n    this.sha256Thumbprint,\n    this.bytes,\n    this.plainSha1,\n    this.algorithmReadableName,\n    this.parameter,\n    this.parameterReadableName,\n    this.exponent,\n  });\n\n  static Uint8List? plainSha1FromJson(List<int>? json) {\n    if (json == null) {\n      return null;\n    }\n    return Uint8List.fromList(json);\n  }\n\n  static List<int>? plainSha1ToJson(Uint8List? object) {\n    if (object == null) {\n      return null;\n    }\n    return object.toList();\n  }\n\n  X509CertificatePublicKeyData.fromSubjectPublicKeyInfo(\n      SubjectPublicKeyInfo info) {\n    algorithm = info.algorithm;\n    length = info.length;\n    sha1Thumbprint = info.sha1Thumbprint;\n    sha256Thumbprint = info.sha256Thumbprint;\n    bytes = info.bytes;\n    algorithmReadableName = info.algorithmReadableName;\n    parameter = info.parameter;\n    parameterReadableName = info.parameterReadableName;\n    exponent = info.exponent;\n  }\n}\n\nclass VmcData {\n  /// The base64 encoded logo\n  String? base64Logo;\n\n  /// The logo type\n  String? type;\n\n  /// The hash\n  String? hash;\n\n  /// The readable version of the algorithm of the hash\n  String? hashAlgorithmReadable;\n\n  /// The algorithm of the hash\n  String? hashAlgorithm;\n\n  VmcData({\n    this.base64Logo,\n    this.hash,\n    this.hashAlgorithm,\n    this.hashAlgorithmReadable,\n    this.type,\n  });\n\n  String getFullSvgData() {\n    return 'data:$type;base64,$base64Logo';\n  }\n}\n"
  },
  {
    "path": "lib/network/util/cert/der.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:convert';\nimport 'package:proxypin/network/util/byte_buf.dart';\nimport 'package:pointycastle/asn1.dart';\nimport 'package:pointycastle/src//utils.dart';\n\nclass DerValue {\n  /// Tag value indicating an ASN.1 \"INTEGER\" value.\n  static const int tagInteger = 0x02;\n\n  /// Tag value indicating an ASN.1 \"OCTET STRING\" value.\n  static const int tagOctetString = 0x04;\n\n  int tag;\n  final Uint8List value;\n  final ByteBuf buffer;\n  late DerInputStream data;\n\n  DerValue(this.tag, this.value, {ByteBuf? buffer}) : buffer = buffer ?? ByteBuf(value) {\n    data = DerInputStream(this.buffer);\n  }\n\n  factory DerValue.fromBytes(Uint8List bytes) {\n    return DerValue.getDerValue(ByteBuf(bytes));\n  }\n\n  factory DerValue.getDerValue(ByteBuf inStream) {\n    var tag = inStream.read();\n    int length = DerInputStream.getLength(inStream);\n    var buffer = inStream.dup();\n    buffer.truncate(length);\n\n    var value = inStream.readBytes(length);\n\n    return DerValue(tag, value, buffer: buffer);\n  }\n\n  Uint8List toByteArray() {\n    DerOutputStream out = DerOutputStream();\n    encode(out);\n    return out.toByteArray();\n  }\n\n  void encode(DerOutputStream out) {\n    out.writeByte(tag);\n    out.writeLength(value.length);\n    out.writeBytes(value);\n  }\n\n  /// Returns true iff the CONSTRUCTED bit is set in the type tag.\n  bool isConstructed() {\n    return ((tag & 0x020) == 0x020);\n  }\n\n  bool isConstructedTag(int constructedTag) {\n    if (!isConstructed()) {\n      return false;\n    }\n    return ((tag & 0x01f) == constructedTag);\n  }\n\n  Uint8List getOctetString() {\n    if (tag != tagOctetString && !isConstructedTag(tagOctetString)) {\n      throw Exception(\"DerValue.getOctetString, not an Octet String: $tag\");\n    }\n\n    if (isConstructed()) {\n      while (data.buffer.isReadable()) {\n        return data.getOctetString();\n      }\n    }\n\n    return value;\n  }\n\n  //get oid\n  ASN1ObjectIdentifier getOID() {\n    if (tag != 0x06) {\n      throw Exception('DER input, Object Identifier tag error');\n    }\n    int length = value.length;\n    int first = value[0] ~/ 40;\n    int second = value[0] % 40;\n    int oid = first * 40 + second;\n    for (int i = 1; i < length; i++) {\n      int byte = value[i];\n      if (byte < 128) {\n        oid = oid * 128 + byte;\n      } else {\n        oid = oid * 128 + (byte & 0x7F);\n      }\n    }\n    return ASN1ObjectIdentifier.fromIdentifierString(oid.toString());\n  }\n\n  DerInputStream toDerInputStream() {\n    return data;\n  }\n\n  @override\n  String toString() {\n    return 'DerValue(tag: $tag, value: ${base64.encode(value)})';\n  }\n}\n\nclass DerOutputStream {\n  final BytesBuilder _builder = BytesBuilder();\n\n  void writeByte(int byte) {\n    _builder.addByte(byte);\n  }\n\n  void writeLength(int length) {\n    if (length < 128) {\n      _builder.addByte(length);\n    } else {\n      int numBytes = (length.bitLength + 7) >> 3;\n      _builder.addByte(0x80 | numBytes);\n      for (int i = numBytes - 1; i >= 0; i--) {\n        _builder.addByte((length >> (8 * i)) & 0xFF);\n      }\n    }\n  }\n\n  void writeBytes(Uint8List bytes) {\n    _builder.add(bytes);\n  }\n\n  Uint8List toByteArray() {\n    return _builder.toBytes();\n  }\n}\n\nclass DerInputStream {\n  final ByteBuf buffer;\n\n  DerInputStream(this.buffer);\n\n  factory DerInputStream.fromBytes(Uint8List data) {\n    return DerInputStream(ByteBuf(data));\n  }\n\n  static int getLength(ByteBuf inStream) {\n    int length = inStream.read();\n    if (length & 0x80 == 0) {\n      return length;\n    }\n    int numBytes = length & 0x7F;\n    length = 0;\n    for (int i = 0; i < numBytes; i++) {\n      length = (length << 8) | inStream.read();\n    }\n    return length;\n  }\n\n  int getInteger() {\n    if (buffer.read() != DerValue.tagInteger) {\n      throw Exception(\"DER input, Integer tag error\");\n    }\n    var length = getLength(buffer);\n    return decodeBigInt(buffer.readBytes(length)).toInt();\n  }\n\n  List<DerValue> getSequence(int startLen) {\n    int tag = buffer.read();\n    if (tag != 0x30) {\n      // SEQUENCE tag\n      throw Exception('Sequence tag error');\n    }\n\n    int length = getLength(buffer);\n    Uint8List sequenceData = buffer.readBytes(length);\n    DerInputStream sequenceStream = DerInputStream.fromBytes(sequenceData);\n\n    List<DerValue> values = [];\n    while (sequenceStream.buffer.isReadable()) {\n      int valueTag = sequenceStream.buffer.read();\n      int valueLength = getLength(sequenceStream.buffer);\n      Uint8List valueData = sequenceStream.buffer.readBytes(valueLength);\n      values.add(DerValue(valueTag, valueData));\n    }\n    return values;\n  }\n\n  ASN1ObjectIdentifier getOID() {\n    var oid = ASN1ObjectIdentifier.fromBytes(buffer.bytes);\n    buffer.read();\n    var length = getLength(buffer);\n    buffer.skipBytes(length);\n    return oid;\n  }\n\n  DerValue getDerValue() {\n    return DerValue.getDerValue(buffer);\n  }\n\n  /// Returns an ASN.1 OCTET STRING from the input stream.\n  Uint8List getOctetString() {\n    if (buffer.read() != DerValue.tagOctetString) {\n      throw Exception(\"DER input not an octet string\");\n    }\n\n    int length = getLength(buffer);\n    return buffer.readBytes(length);\n  }\n}\n\nclass DerIndefLenConverter {\n  static const int LEN_LONG = 0x80; // bit 8 set\n  static const int LEN_MASK = 0x7f; // bits 7 - 1\n\n  late Uint8List data;\n  late Uint8List newData;\n  int newDataPos = 0, dataPos = 0, dataSize = 0, index = 0;\n  int unresolved = 0;\n  List<Object> ndefsList = [];\n  int numOfTotalLenBytes = 0;\n\n  static bool isEOC(Uint8List data, int pos) {\n    return data[pos] == 0 && data[pos + 1] == 0;\n  }\n\n  static bool isLongForm(int lengthByte) {\n    return (lengthByte & LEN_LONG) == LEN_LONG;\n  }\n\n  static bool isIndefinite(int lengthByte) {\n    return (isLongForm(lengthByte) && ((lengthByte & LEN_MASK) == 0));\n  }\n\n  void parseTag() {\n    if (isEOC(data, dataPos)) {\n      int numOfEncapsulatedLenBytes = 0;\n      var elem;\n      int index;\n      for (index = ndefsList.length - 1; index >= 0; index--) {\n        elem = ndefsList[index];\n        if (elem is int) {\n          break;\n        } else {\n          numOfEncapsulatedLenBytes += (elem as Uint8List).length - 3;\n        }\n      }\n      if (index < 0) {\n        throw Exception(\"EOC does not have matching indefinite-length tag\");\n      }\n\n      int sectionLen = dataPos - (elem as int) + numOfEncapsulatedLenBytes;\n      Uint8List sectionLenBytes = getLengthBytes(sectionLen);\n      ndefsList[index] = sectionLenBytes;\n      unresolved--;\n\n      numOfTotalLenBytes += (sectionLenBytes.length - 3);\n    }\n    dataPos++;\n  }\n\n  void writeTag() {\n    while (dataPos < dataSize) {\n      if (isEOC(data, dataPos)) {\n        dataPos += 2;\n      } else {\n        newData[newDataPos++] = data[dataPos++];\n        break;\n      }\n    }\n  }\n\n  int parseLength() {\n    if (dataPos == dataSize) {\n      return 0;\n    }\n    int lenByte = data[dataPos++] & 0xff;\n    if (isIndefinite(lenByte)) {\n      ndefsList.add(dataPos);\n      unresolved++;\n      return 0;\n    }\n    int curLen = 0;\n    if (isLongForm(lenByte)) {\n      lenByte &= LEN_MASK;\n      if (lenByte > 4) {\n        throw Exception(\"Too much data\");\n      }\n      if ((dataSize - dataPos) < (lenByte + 1)) {\n        return -1;\n      }\n      for (int i = 0; i < lenByte; i++) {\n        curLen = (curLen << 8) + (data[dataPos++] & 0xff);\n      }\n      if (curLen < 0) {\n        throw Exception(\"Invalid length bytes\");\n      }\n    } else {\n      curLen = (lenByte & LEN_MASK);\n    }\n    return curLen;\n  }\n\n  void writeLengthAndValue() {\n    if (dataPos == dataSize) {\n      return;\n    }\n    int curLen = 0;\n    int lenByte = data[dataPos++] & 0xff;\n    if (isIndefinite(lenByte)) {\n      Uint8List lenBytes = ndefsList[index++] as Uint8List;\n      newData.setRange(newDataPos, newDataPos + lenBytes.length, lenBytes);\n      newDataPos += lenBytes.length;\n    } else {\n      if (isLongForm(lenByte)) {\n        lenByte &= LEN_MASK;\n        for (int i = 0; i < lenByte; i++) {\n          curLen = (curLen << 8) + (data[dataPos++] & 0xff);\n        }\n        if (curLen < 0) {\n          throw Exception(\"Invalid length bytes\");\n        }\n      } else {\n        curLen = (lenByte & LEN_MASK);\n      }\n      writeLength(curLen);\n      writeValue(curLen);\n    }\n  }\n\n  void writeLength(int curLen) {\n    if (curLen < 128) {\n      newData[newDataPos++] = curLen;\n    } else if (curLen < (1 << 8)) {\n      newData[newDataPos++] = 0x81;\n      newData[newDataPos++] = curLen;\n    } else if (curLen < (1 << 16)) {\n      newData[newDataPos++] = 0x82;\n      newData[newDataPos++] = (curLen >> 8);\n      newData[newDataPos++] = curLen;\n    } else if (curLen < (1 << 24)) {\n      newData[newDataPos++] = 0x83;\n      newData[newDataPos++] = (curLen >> 16);\n      newData[newDataPos++] = (curLen >> 8);\n      newData[newDataPos++] = curLen;\n    } else {\n      newData[newDataPos++] = 0x84;\n      newData[newDataPos++] = (curLen >> 24);\n      newData[newDataPos++] = (curLen >> 16);\n      newData[newDataPos++] = (curLen >> 8);\n      newData[newDataPos++] = curLen;\n    }\n  }\n\n  Uint8List getLengthBytes(int curLen) {\n    Uint8List lenBytes;\n    int index = 0;\n\n    if (curLen < 128) {\n      lenBytes = Uint8List(1);\n      lenBytes[index++] = curLen;\n    } else if (curLen < (1 << 8)) {\n      lenBytes = Uint8List(2);\n      lenBytes[index++] = 0x81;\n      lenBytes[index++] = curLen;\n    } else if (curLen < (1 << 16)) {\n      lenBytes = Uint8List(3);\n      lenBytes[index++] = 0x82;\n      lenBytes[index++] = (curLen >> 8);\n      lenBytes[index++] = curLen;\n    } else if (curLen < (1 << 24)) {\n      lenBytes = Uint8List(4);\n      lenBytes[index++] = 0x83;\n      lenBytes[index++] = (curLen >> 16);\n      lenBytes[index++] = (curLen >> 8);\n      lenBytes[index++] = curLen;\n    } else {\n      lenBytes = Uint8List(5);\n      lenBytes[index++] = 0x84;\n      lenBytes[index++] = (curLen >> 24);\n      lenBytes[index++] = (curLen >> 16);\n      lenBytes[index++] = (curLen >> 8);\n      lenBytes[index++] = curLen;\n    }\n\n    return lenBytes;\n  }\n\n  void writeValue(int curLen) {\n    newData.setRange(newDataPos, newDataPos + curLen, data, dataPos);\n    dataPos += curLen;\n    newDataPos += curLen;\n  }\n\n  Uint8List? convertBytes(Uint8List indefData) {\n    data = indefData;\n    dataPos = 0;\n    dataSize = data.length;\n\n    while (dataPos < dataSize) {\n      if (dataPos + 2 > dataSize) {\n        return null;\n      }\n      parseTag();\n      int len = parseLength();\n      if (len < 0) {\n        return null;\n      }\n      dataPos += len;\n      if (dataPos < 0) {\n        throw Exception(\"Data overflow\");\n      }\n      if (unresolved == 0) {\n        break;\n      }\n    }\n\n    if (unresolved != 0) {\n      return null;\n    }\n\n    int unused = dataSize - dataPos;\n    dataSize = dataPos;\n\n    newData = Uint8List(dataSize + numOfTotalLenBytes + unused);\n    dataPos = 0;\n    newDataPos = 0;\n    index = 0;\n\n    while (dataPos < dataSize) {\n      writeTag();\n      writeLengthAndValue();\n    }\n    newData.setRange(dataSize + numOfTotalLenBytes, newData.length, data, dataSize);\n\n    return newData;\n  }\n}\n"
  },
  {
    "path": "lib/network/util/cert/extension.dart",
    "content": "import 'package:pointycastle/pointycastle.dart';\n\n/// an object for the elements in the X.509 V3 extension block.\nclass Extension {\n  /// Key Usage\n  static final ASN1ObjectIdentifier keyUsage = ASN1ObjectIdentifier.fromIdentifierString(\"2.5.29.15\");\n\n  /// Subject Alternative Name\n  static final ASN1ObjectIdentifier subjectAlternativeName = ASN1ObjectIdentifier.fromIdentifierString(\"2.5.29.17\");\n\n  /// Basic Constraints\n  static final ASN1ObjectIdentifier basicConstraints = ASN1ObjectIdentifier.fromIdentifierString(\"2.5.29.19\");\n\n  /// Extended Key Usage\n  static final ASN1ObjectIdentifier extendedKeyUsage = ASN1ObjectIdentifier.fromIdentifierString(\"2.5.29.37\");\n\n  final ASN1ObjectIdentifier extnId;\n  final bool critical;\n\n  final ASN1OctetString value;\n\n  Extension(this.extnId, this.critical, this.value);\n}\n\nenum ExtendedKeyUsage {\n  SERVER_AUTH,\n  CLIENT_AUTH,\n  CODE_SIGNING,\n  EMAIL_PROTECTION,\n  TIME_STAMPING,\n  OCSP_SIGNING,\n  BIMI\n}"
  },
  {
    "path": "lib/network/util/cert/key_usage.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:pointycastle/pointycastle.dart';\n\nenum KeyUsage {\n  /// 0\n  DIGITAL_SIGNATURE,\n\n  /// 1 (Also called contentCommitment now)\n  NON_REPUDIATION,\n\n  /// 2\n  KEY_ENCIPHERMENT,\n\n  /// 3\n  DATA_ENCIPHERMENT,\n\n  /// 4\n  KEY_AGREEMENT,\n\n  /// 5\n  KEY_CERT_SIGN,\n\n  /// 6\n  CRL_SIGN,\n\n  /// 7\n  ENCIPHER_ONLY,\n\n  /// 8\n  DECIPHER_ONLY\n}\n\nclass ExtensionKeyUsage {\n  static const int digitalSignature = (1 << 7);\n  static const int nonRepudiation = (1 << 6);\n  static const int keyEncipherment = (1 << 5);\n  static const int dataEncipherment = (1 << 4);\n  static const int keyAgreement = (1 << 3);\n  static const int keyCertSign = (1 << 2);\n  static const int cRLSign = (1 << 1);\n  static const int encipherOnly = (1 << 0);\n  static const int decipherOnly = (1 << 15);\n\n  final ASN1BitString bitString;\n  final bool critical;\n\n  ExtensionKeyUsage(int usage, {this.critical = true}) : bitString = ASN1BitString.fromBytes(keyUsageBytes(usage));\n\n  static Uint8List keyUsageBytes(int valueBytes) {\n    var bytes = [valueBytes];\n    if (valueBytes > 0xFF) {\n      final int firstValueByte = (valueBytes & int.parse(\"ff00\", radix: 16)) >> 8;\n      final int secondValueByte = (valueBytes & int.parse(\"00ff\", radix: 16));\n      bytes = [firstValueByte, secondValueByte];\n    }\n\n    return Uint8List.fromList(<int>[\n      // BitString identifier\n      3,\n      // Length\n      bytes.length + 1,\n      // Unused bytes at the end\n      1,\n      ...bytes\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/network/util/cert/pkcs12.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:pointycastle/asn1.dart';\nimport 'package:pointycastle/export.dart';\n\nimport '../crypto.dart';\nimport '../lang.dart';\nimport 'der.dart';\nimport 'x509.dart';\n\nclass Pkcs12 {\n  ///\n  /// Generates a PKCS12 file according to RFC 7292.\n  ///\n  /// * privateKey = A private key in PEM format.\n  /// * certificates = A list of certificates in PEM format.\n  /// * password = The password used for encryption.\n  /// * keyPbe = The encryption algorithm used to encrypt the private key.\n  /// * certPbe = The encryption algorithm used to encrypt the certificates.\n  /// * digetAlgorithm = The digest algorithm used for the mac key derivation\n  /// * macIter = The iteration count for the key derivation\n  /// * salt = The salt used for the key derivation, if left out, it will be generated\n  /// * certSalt = The salt used for the key derivation for cert encryption, if left out salt will be used.\n  /// * keySalt = The salt used for the key derivation for key encryption, if left out salt will be used.\n  /// * friendlyName =  The name to be used to place as an attribue.\n  /// * localKeyId = The id to be used to place as an attribue. If left, it will be generated.\n  ///\n  /// Possible values for keyPbe and certPbe:\n  /// * PBE-SHA1-RC4-128\n  /// * PBE-SHA1-RC4-40\n  /// * PBE-SHA1-3DES ( default for keyPbe )\n  /// * PBE-SHA1-2DES\n  /// * PBE-SHA1-RC2-128\n  /// * PBE-SHA1-RC2-40 ( default for certPbe)\n  ///\n  /// Possible values for digestAlgorithm:\n  /// * SHA-1 ( DEFAULT)\n  /// * SHA-224\n  /// * SHA-256\n  /// * SHA-384\n  /// * SHA-512\n  ///\n  /// **IMPORTANT:** This method generates a PKCS12 file that only supports PASSWORD PRIVACY and PASSWORD INTEGRITY mode. This\n  /// means that the private key and certificates are encrypted with the given password and the HMAC is generated using the given password.\n  ///\n  /// If keyPbe or certPbe are set to NONE or the password is left out, there will be no encryption.\n  /// If the password is left out, no HMAC is generated\n  ///\n  ///\n  static Uint8List generatePkcs12(\n    String privateKey,\n    List<String> certificates, {\n    String? password,\n    String keyPbe = 'PBE-SHA1-3DES',\n    String certPbe = 'PBE-SHA1-RC2-40',\n    String digestAlgorithm = 'SHA-1',\n    int macIter = 2048,\n    Uint8List? salt,\n    Uint8List? certSalt,\n    Uint8List? keySalt,\n    String? friendlyName,\n    Uint8List? localKeyId,\n  }) {\n    Uint8List? pwFormatted;\n    if (password != null) {\n      pwFormatted = formatPkcs12Password(Uint8List.fromList(password.codeUnits));\n    }\n\n    // GENERATE SALT\n    salt ??= _generateSalt();\n\n    certSalt ??= salt;\n\n    keySalt ??= salt;\n\n    // GENERATE LOCAL KEY ID\n    localKeyId ??= _generateLocalKeyId();\n\n    // CREATE SAFEBAGS WITH PEMS WRAPPED IN CERTBAG\n    var safeBags = _generateSafeBagsForCerts(certificates, localKeyId, friendlyName: friendlyName);\n    var safeContentsCert = ASN1SafeContents(safeBags);\n\n    // CREATE CONTENT INFO\n    ASN1ContentInfo contentInfoCert;\n    ASN1ContentInfo contentInfoKey;\n    if (certPbe != 'NONE' && pwFormatted != null) {\n      var params = ASN1Sequence(\n        elements: [\n          ASN1OctetString(octets: certSalt),\n          ASN1Integer(\n            BigInt.from(macIter),\n          ),\n        ],\n      );\n      var contentEncryptionAlgorithm = ASN1AlgorithmIdentifier(\n        _oiFromAlgorithm(certPbe),\n        parameters: params,\n      );\n\n      Uint8List encryptedContent = _encrypt(\n        safeContentsCert.encode(),\n        certPbe,\n        pwFormatted,\n        certSalt,\n        macIter,\n        'SHA-1',\n      );\n\n      var encryptedContentInfo = ASN1EncryptedContentInfo.forData(contentEncryptionAlgorithm, encryptedContent);\n\n      var encryptedData = ASN1EncryptedData(encryptedContentInfo);\n      contentInfoCert = ASN1ContentInfo.forEncryptedData(encryptedData);\n    } else {\n      contentInfoCert = ASN1ContentInfo.forData(\n        ASN1OctetString(\n          octets: safeContentsCert.encode(),\n        ),\n      );\n    }\n    if (keyPbe != 'NONE' && pwFormatted != null) {\n      var params = ASN1Sequence(elements: [\n        ASN1OctetString(octets: keySalt),\n        ASN1Integer(BigInt.from(macIter)),\n      ]);\n      var contentEncryptionAlgorithm = ASN1AlgorithmIdentifier(\n        _oiFromAlgorithm(keyPbe),\n        parameters: params,\n      );\n      var privateKeyInfo = _getPrivateKeyInfoFromPem(privateKey);\n      Uint8List encryptedContent = _encrypt(\n        privateKeyInfo.encode(),\n        keyPbe,\n        pwFormatted,\n        keySalt,\n        macIter,\n        'SHA-1',\n      );\n\n      // CREATE SAFEBAG FOR PRIVATEKEY WRAPPED IN KEYBAG\n      var safeBagsKey = _generateSafeBagsForShroudedKey(\n        ASN1Sequence(elements: [contentEncryptionAlgorithm, ASN1OctetString(octets: encryptedContent)]),\n        localKeyId,\n        friendlyName: friendlyName,\n      );\n\n      var safeContentsKey = ASN1SafeContents(safeBagsKey);\n      contentInfoKey = ASN1ContentInfo.forData(\n        ASN1OctetString(\n          octets: safeContentsKey.encode(),\n        ),\n      );\n    } else {\n      // CREATE SAFEBAG FOR PRIVATEKEY WRAPPED IN KEYBAG\n      var safeBagsKey = _generateSafeBagsForKey(\n        privateKey,\n        localKeyId,\n        friendlyName: friendlyName,\n      );\n\n      var safeContentsKey = ASN1SafeContents(safeBagsKey);\n\n      contentInfoKey = ASN1ContentInfo.forData(\n        ASN1OctetString(\n          octets: safeContentsKey.encode(),\n        ),\n      );\n    }\n\n    // CREATE AUTHENTICATED SAFE WITH CONTENTINFO ( CERT AND KEY )\n    var authSafe = ASN1AuthenticatedSafe([contentInfoCert, contentInfoKey]);\n\n    // WRAP AUTHENTICATED SAFE WITHIN A CONTENTINFO\n    var T = ASN1ContentInfo.forData(\n      ASN1OctetString(\n        octets: authSafe.encode(),\n      ),\n    );\n\n    // GENERATE HMAC IF PASSWORD IS GIVEN\n    ASN1MacData? macData;\n    if (password != null) {\n      var bytesForHmac = authSafe.encode();\n\n      var pwFormatted = formatPkcs12Password(Uint8List.fromList(password.codeUnits));\n\n      var generator = PKCS12ParametersGenerator(Digest(digestAlgorithm));\n      generator.init(pwFormatted, salt, macIter);\n\n      var key = generator.generateDerivedMacParameters(20);\n      var m = _generateHmac(bytesForHmac, key.key, digestAlgorithm);\n      macData = ASN1MacData(\n        ASN1DigestInfo(\n          m,\n          _algorithmIdentifierFromDigest(\n            digestAlgorithm,\n          ),\n        ),\n        salt,\n        BigInt.from(2048),\n      );\n    }\n    var pfx = ASN1Pfx(\n      ASN1Integer(BigInt.from(3)),\n      T,\n      macData: macData,\n    );\n    var bytes = pfx.encode();\n    return bytes;\n  }\n\n  static Uint8List _generateLocalKeyId() {\n    return CryptoUtils.getSecureRandom().nextBytes(20);\n  }\n\n  static Uint8List _generateSalt() {\n    return CryptoUtils.getSecureRandom().nextBytes(8);\n  }\n\n  static Uint8List _generateHmac(Uint8List bytesForHmac, Uint8List key, String digestAlgorithm) {\n    final hmac = Mac('$digestAlgorithm/HMAC')..init(KeyParameter(key));\n    var m = hmac.process(bytesForHmac);\n    return m;\n  }\n\n  ///\n  /// Formats the given [password] according to RFC 7292 Appendix B.1\n  ///\n  static Uint8List formatPkcs12Password(Uint8List password) {\n    if (password.isNotEmpty) {\n      // +1 for extra 2 pad bytes.\n      var bytes = Uint8List((password.length + 1) * 2);\n\n      for (var i = 0; i != password.length; i++) {\n        bytes[i * 2] = (password[i] >>> 8);\n        bytes[i * 2 + 1] = password[i];\n      }\n\n      return bytes;\n    } else {\n      return Uint8List(0);\n    }\n  }\n\n  static _generateSafeBagsForCerts(List<String> certificates, Uint8List localKeyId, {String? friendlyName}) {\n    var certBags = <ASN1CertBag>[];\n    var safeBags = <ASN1SafeBag>[];\n\n    for (var pem in certificates) {\n      certBags.add(ASN1CertBag.fromX509Pem(pem));\n    }\n    for (var certBag in certBags) {\n      var asn1Set = ASN1Set(elements: []);\n      asn1Set.add(ASN1Pkcs12Attribute.localKeyID(localKeyId));\n      if (friendlyName != null) {\n        asn1Set.add(ASN1Pkcs12Attribute.friendlyName(friendlyName));\n      }\n      safeBags.add(\n        ASN1SafeBag.forCertBag(\n          certBag,\n          bagAttributes: asn1Set,\n        ),\n      );\n    }\n    return safeBags;\n  }\n\n  static List<ASN1SafeBag> _generateSafeBagsForKey(String privateKey, Uint8List localKeyId, {String? friendlyName}) {\n    late ASN1PrivateKeyInfo privateKeyInfo = _getPrivateKeyInfoFromPem(privateKey);\n\n    var safeBagsKey = <ASN1SafeBag>[];\n    var asn1Set = ASN1Set(elements: []);\n    asn1Set.add(ASN1Pkcs12Attribute.localKeyID(localKeyId));\n    if (friendlyName != null) {\n      asn1Set.add(ASN1Pkcs12Attribute.friendlyName(friendlyName));\n    }\n    safeBagsKey.add(\n      ASN1SafeBag.forKeyBag(\n        ASN1KeyBag(privateKeyInfo),\n        bagAttributes: asn1Set,\n      ),\n    );\n    return safeBagsKey;\n  }\n\n  static _generateSafeBagsForShroudedKey(ASN1Object bagValue, Uint8List localKeyId, {String? friendlyName}) {\n    var safeBagsKey = <ASN1SafeBag>[];\n    var asn1Set = ASN1Set(elements: []);\n    asn1Set.add(ASN1Pkcs12Attribute.localKeyID(localKeyId));\n    if (friendlyName != null) {\n      asn1Set.add(ASN1Pkcs12Attribute.friendlyName(friendlyName));\n    }\n    safeBagsKey.add(\n      ASN1SafeBag.forPkcs8ShroudedKeyBag(\n        bagValue,\n        bagAttributes: asn1Set,\n      ),\n    );\n    return safeBagsKey;\n  }\n\n  static ASN1PrivateKeyInfo _getPrivateKeyInfoFromPem(String pem) {\n    late ASN1PrivateKeyInfo privateKeyInfo;\n    switch (CryptoUtils.getPrivateKeyType(pem)) {\n      case \"RSA\":\n        privateKeyInfo = ASN1PrivateKeyInfo.fromPkcs8RsaPem(pem);\n        break;\n      case \"RSA_PKCS1\":\n        privateKeyInfo = ASN1PrivateKeyInfo.fromPkcs1RsaPem(pem);\n        break;\n      case \"ECC\":\n        privateKeyInfo = ASN1PrivateKeyInfo.fromEccPem(pem);\n        break;\n    }\n    return privateKeyInfo;\n  }\n\n  static Uint8List _encryptRc2(Uint8List bytesToEncrypt, ParametersWithIV generateDerivedParametersWithIV) {\n    return _processRc2(bytesToEncrypt, generateDerivedParametersWithIV, true);\n  }\n\n  static Uint8List _decryptRc2(Uint8List bytesToDecrypt, ParametersWithIV generateDerivedParametersWithIV) {\n    return _processRc2(bytesToDecrypt, generateDerivedParametersWithIV, false);\n  }\n\n  static Uint8List _processRc2(Uint8List bytes, ParametersWithIV generateDerivedParametersWithIV, bool encrypt) {\n    var engine = CBCBlockCipher(RC2Engine());\n    engine.reset();\n    engine.init(encrypt, generateDerivedParametersWithIV);\n    var padded = CryptoUtils.addPKCS7Padding(bytes, 8);\n    final encryptedContent = Uint8List(padded.length);\n\n    var offset = 0;\n    while (offset < padded.length) {\n      offset += engine.processBlock(padded, offset, encryptedContent, offset);\n    }\n\n    return encryptedContent;\n  }\n\n  static Uint8List _encrypt3des(Uint8List bytesToEncrypt, ParametersWithIV generateDerivedParametersWithIV) {\n    return _process3des(bytesToEncrypt, generateDerivedParametersWithIV, true);\n  }\n\n  static Uint8List _decrypt3des(Uint8List bytesToDecrypt, ParametersWithIV generateDerivedParametersWithIV) {\n    return _process3des(bytesToDecrypt, generateDerivedParametersWithIV, false);\n  }\n\n  static Uint8List _process3des(Uint8List bytes, ParametersWithIV generateDerivedParametersWithIV, bool encrypt) {\n    var engine = CBCBlockCipher(DESedeEngine());\n    engine.reset();\n    engine.init(encrypt, generateDerivedParametersWithIV);\n    Uint8List padded;\n    if (encrypt) {\n      padded = CryptoUtils.addPKCS7Padding(bytes, 8);\n    } else {\n      padded = bytes;\n    }\n\n    final content = Uint8List(padded.length);\n\n    var offset = 0;\n    while (offset < padded.length) {\n      offset += engine.processBlock(padded, offset, content, offset);\n    }\n    if (encrypt) {\n      return content;\n    } else {\n      return CryptoUtils.removePKCS7Padding(content);\n    }\n  }\n\n  static Uint8List _encryptRc4(Uint8List bytesToEncrypt, KeyParameter generateDerivedParameters) {\n    return _processRc4(bytesToEncrypt, generateDerivedParameters, true);\n  }\n\n  static Uint8List _decryptRc4(Uint8List bytesToDecrypt, KeyParameter generateDerivedParameters) {\n    return _processRc4(bytesToDecrypt, generateDerivedParameters, false);\n  }\n\n  static Uint8List _processRc4(Uint8List bytesToEncrypt, KeyParameter generateDerivedParameters, bool encrypt) {\n    var engine = RC4Engine();\n    engine.init(true, generateDerivedParameters);\n    engine.reset();\n    //var padded = CryptoUtils.addPKCS7Padding(bytesToEncrypt, 8);\n    final encryptedContent = engine.process(bytesToEncrypt);\n\n    return encryptedContent;\n  }\n\n  static Uint8List _encrypt(\n      Uint8List encode, String algorithm, Uint8List pwFormatted, Uint8List salt, int macIter, String digetAlgorithm) {\n    var pkcs12ParameterGenerator = PKCS12ParametersGenerator(Digest(digetAlgorithm));\n    pkcs12ParameterGenerator.init(pwFormatted, salt, macIter);\n\n    switch (algorithm) {\n      case 'PBE-SHA1-RC2-40':\n        return _encryptRc2(\n          encode,\n          pkcs12ParameterGenerator.generateDerivedParametersWithIV(5, RC2Engine.BLOCK_SIZE),\n        );\n      case 'PBE-SHA1-RC2-128':\n        return _encryptRc2(\n          encode,\n          pkcs12ParameterGenerator.generateDerivedParametersWithIV(16, RC2Engine.BLOCK_SIZE),\n        );\n      case 'PBE-SHA1-RC4-40':\n        return _encryptRc4(\n          encode,\n          pkcs12ParameterGenerator.generateDerivedParameters(5),\n        );\n      case 'PBE-SHA1-RC4-128':\n        return _encryptRc4(\n          encode,\n          pkcs12ParameterGenerator.generateDerivedParameters(16),\n        );\n      case 'PBE-SHA1-2DES':\n        return _encrypt3des(\n          encode,\n          pkcs12ParameterGenerator.generateDerivedParametersWithIV(\n            16,\n            DESedeEngine.BLOCK_SIZE,\n          ),\n        );\n      case 'PBE-SHA1-3DES':\n        return _encrypt3des(\n          encode,\n          pkcs12ParameterGenerator.generateDerivedParametersWithIV(\n            24,\n            DESedeEngine.BLOCK_SIZE,\n          ),\n        );\n      default:\n        throw ArgumentError('unsupported algorithm $algorithm');\n    }\n  }\n\n  static Uint8List _decrypt(Uint8List toDecrypt, String algorithm, Uint8List pwFormatted, Uint8List salt, int macIter,\n      String digetAlgorithm) {\n    var pkcs12ParameterGenerator = PKCS12ParametersGenerator(Digest(digetAlgorithm));\n    pkcs12ParameterGenerator.init(pwFormatted, salt, macIter);\n\n    switch (algorithm) {\n      case 'PBE-SHA1-RC2-40':\n        return _decryptRc2(\n          toDecrypt,\n          pkcs12ParameterGenerator.generateDerivedParametersWithIV(5, RC2Engine.BLOCK_SIZE),\n        );\n      case 'PBE-SHA1-RC2-128':\n        return _decryptRc2(\n          toDecrypt,\n          pkcs12ParameterGenerator.generateDerivedParametersWithIV(16, RC2Engine.BLOCK_SIZE),\n        );\n      case 'PBE-SHA1-RC4-40':\n        return _decryptRc4(\n          toDecrypt,\n          pkcs12ParameterGenerator.generateDerivedParameters(5),\n        );\n      case 'PBE-SHA1-RC4-128':\n        return _decryptRc4(\n          toDecrypt,\n          pkcs12ParameterGenerator.generateDerivedParameters(16),\n        );\n      case 'PBE-SHA1-2DES':\n        return _decrypt3des(\n          toDecrypt,\n          pkcs12ParameterGenerator.generateDerivedParametersWithIV(\n            16,\n            DESedeEngine.BLOCK_SIZE,\n          ),\n        );\n      case 'PBE-SHA1-3DES':\n        return _decrypt3des(\n          toDecrypt,\n          pkcs12ParameterGenerator.generateDerivedParametersWithIV(\n            24,\n            DESedeEngine.BLOCK_SIZE,\n          ),\n        );\n      default:\n        throw ArgumentError('unsupported algorithm $algorithm');\n    }\n  }\n\n  static ASN1AlgorithmIdentifier _algorithmIdentifierFromDigest(String digestAlgorithm) {\n    switch (digestAlgorithm) {\n      case 'SHA-1':\n        return ASN1AlgorithmIdentifier.fromIdentifier('1.3.14.3.2.26');\n      case 'SHA-224':\n        return ASN1AlgorithmIdentifier.fromIdentifier('2.16.840.1.101.3.4.2.4');\n      case 'SHA-256':\n        return ASN1AlgorithmIdentifier.fromIdentifier('2.16.840.1.101.3.4.2.1');\n      case 'SHA-384':\n        return ASN1AlgorithmIdentifier.fromIdentifier('2.16.840.1.101.3.4.2.2');\n      case 'SHA-512':\n        return ASN1AlgorithmIdentifier.fromIdentifier('2.16.840.1.101.3.4.2.3');\n      default:\n        return ASN1AlgorithmIdentifier.fromIdentifier('1.3.14.3.2.26');\n    }\n  }\n\n  static ASN1ObjectIdentifier _oiFromAlgorithm(String keyPbe) {\n    switch (keyPbe) {\n      case 'PBE-SHA1-RC2-40':\n        // 1.2.840.113549.1.12.1.6\n        return ASN1ObjectIdentifier.fromBytes(\n          Uint8List.fromList(\n            HexUtils.decode(\"06 0A 2A 86 48 86 F7 0D 01 0C 01 06\"),\n          ),\n        );\n      case 'PBE-SHA1-RC2-128':\n        // 1.2.840.113549.1.12.1.5\n        return ASN1ObjectIdentifier.fromBytes(\n          Uint8List.fromList(\n            HexUtils.decode(\"06 0A 2A 86 48 86 F7 0D 01 0C 01 05\"),\n          ),\n        );\n      case 'PBE-SHA1-RC4-40':\n        // 1.2.840.113549.1.12.1.2\n        return ASN1ObjectIdentifier.fromBytes(\n          Uint8List.fromList(\n            HexUtils.decode(\"06 0A 2A 86 48 86 F7 0D 01 0C 01 02\"),\n          ),\n        );\n      case 'PBE-SHA1-RC4-128':\n        // 1.2.840.113549.1.12.1.1\n        return ASN1ObjectIdentifier.fromBytes(\n          Uint8List.fromList(\n            HexUtils.decode(\"06 0A 2A 86 48 86 F7 0D 01 0C 01 01\"),\n          ),\n        );\n      case 'PBE-SHA1-2DES':\n        // 1.2.840.113549.1.12.1.4\n        return ASN1ObjectIdentifier.fromBytes(\n          Uint8List.fromList(\n            HexUtils.decode(\"06 0A 2A 86 48 86 F7 0D 01 0C 01 04\"),\n          ),\n        );\n      case 'PBE-SHA1-3DES':\n        // 1.2.840.113549.1.12.1.3\n        return ASN1ObjectIdentifier.fromBytes(\n          Uint8List.fromList(\n            HexUtils.decode(\"06 0A 2A 86 48 86 F7 0D 01 0C 01 03\"),\n          ),\n        );\n      default:\n        throw ArgumentError('unsupported algorithm');\n    }\n  }\n\n  ///解析pkcs12文件\n  static List<String> parsePkcs12(\n    Uint8List pkcs12, {\n    String? password,\n  }) {\n    Uint8List? pwFormatted;\n    if (password != null) {\n      pwFormatted = formatPkcs12Password(Uint8List.fromList(password.codeUnits));\n    }\n\n    var pems = <String>[];\n    var parser = ASN1Parser(pkcs12);\n    var wrapperSeq = parser.nextObject() as ASN1Sequence;\n    var pfx = ASN1Pfx.fromSequence(wrapperSeq);\n\n    if (pfx.version.integer != BigInt.from(3)) {\n      throw Exception(\"PKCS12 keystore not in version 3 format\");\n    }\n\n    var authSafeContent = pfx.authSafe.content as ASN1OctetString;\n    parser = ASN1Parser(authSafeContent.valueBytes);\n    ASN1Object asn1Object = parser.nextObject();\n\n    // Check the type before casting\n    if (asn1Object is ASN1Sequence) {\n      wrapperSeq = asn1Object;\n    } else if (asn1Object is ASN1OctetString) {\n      var octetString = authSafeContent;\n      BytesBuilder authSafeData = BytesBuilder();\n      var parser = ASN1Parser(octetString.valueBytes);\n      while (parser.hasNext()) {\n        ASN1Object parsedContent = parser.nextObject() as ASN1OctetString;\n        authSafeData.add(parsedContent.valueBytes!);\n      }\n\n      var data = authSafeData.toBytes();\n      // Check if the data is indefinite\n      if (DerIndefLenConverter.isIndefinite(data[1])) {\n        data = DerIndefLenConverter().convertBytes(data)!;\n      }\n\n      parser = ASN1Parser(data);\n      ASN1Object asn1Object = parser.nextObject();\n      if (asn1Object is ASN1Sequence) {\n        wrapperSeq = asn1Object;\n      } else {\n        throw Exception(\"Invalid PKCS12 keystore\");\n      }\n    }\n\n    for (var e in wrapperSeq.elements!) {\n      if (e is ASN1Sequence) {\n        if (e.elements == null || e.elements!.isEmpty) {\n          throw Exception(\"Invalid PKCS12 keystore\");\n        }\n        var contentInfo = ASN1ContentInfo.fromSequence(e);\n        switch (contentInfo.contentType.objectIdentifierAsString) {\n          case '1.2.840.113549.1.7.6': // encryptedData\n            var encryptedData = ASN1EncryptedData.fromSequence(contentInfo.content as ASN1Sequence);\n            var encryptedContentInfo = encryptedData.encryptedContentInfo;\n\n            var seq = (contentInfo.content as ASN1Sequence).elements!.elementAt(1) as ASN1Sequence;\n            // var startIndex = seq.elements!.elementAt(0).encodedBytes!.lengthInBytes;\n            // startIndex += (seq.elements!.elementAt(1).encodedBytes!.lengthInBytes);\n            // var encrypted = DerValue.fromBytes(seq.valueBytes!.sublist(startIndex));\n            var encrypted = DerValue.fromBytes(seq.elements!.elementAt(2).encodedBytes!);\n\n            int newTag = DerValue.tagOctetString;\n            if (encrypted.isConstructed()) {\n              newTag |= 0x20;\n            }\n            encrypted.tag = newTag;\n            var rawData = encrypted.getOctetString();\n\n            // DECRYPT\n            var contentEncryptionAlgorithm = encryptedContentInfo.contentEncryptionAlgorithm;\n            var decryptedContent = _decryptData(rawData, contentEncryptionAlgorithm, pwFormatted!);\n\n            var contentType = encryptedContentInfo.contentType;\n\n            switch (contentType.objectIdentifierAsString) {\n              case '1.2.840.113549.1.7.1': // CERTIFICATES\n                loadSafeContents(DerInputStream.fromBytes(decryptedContent), pems, pwFormatted);\n                break;\n            }\n\n            break;\n          case '1.2.840.113549.1.7.1': // data (PKCS #7)\n            if (contentInfo.content!.isConstructed == true && contentInfo.content is ASN1OctetString) {\n              var content = contentInfo.content as ASN1OctetString;\n              loadSafeContents(DerInputStream.fromBytes(content.octets!), pems, pwFormatted);\n            } else {\n              var safeContents =\n                  ASN1SafeContents.fromSequence(ASN1Sequence.fromBytes(contentInfo.content!.valueBytes!));\n              for (var element in safeContents.safeBags) {\n                var bagValueSeq = element.bagValue as ASN1Sequence;\n                _parseSafaBag(element.bagId, bagValueSeq, pems, pwFormatted);\n              }\n            }\n            break;\n        }\n      }\n    }\n    return pems;\n  }\n\n  static void loadSafeContents(DerInputStream stream, List<String> pems, Uint8List? pwFormatted) {\n    List<DerValue> safeBags = stream.getSequence(2);\n    int count = safeBags.length;\n\n    for (int i = 0; i < count; i++) {\n      var sbi = safeBags[i].toDerInputStream();\n      var bagId = sbi.getOID();\n      var bagValue = sbi.getDerValue();\n      bagValue = bagValue.data.getDerValue();\n      var data = bagValue.toByteArray();\n      var bagValueSeq = ASN1Sequence.fromBytes(data);\n      _parseSafaBag(bagId, bagValueSeq, pems, pwFormatted);\n    }\n  }\n\n  static void _parseSafaBag(\n      ASN1ObjectIdentifier bagId, ASN1Sequence bagValueSeq, List<String> pems, Uint8List? pwFormatted) {\n    //private key\n    if (bagId.objectIdentifierAsString == \"1.2.840.113549.1.12.10.1.2\") {\n      var contentEncryptionAlgorithm =\n          ASN1AlgorithmIdentifier.fromSequence(bagValueSeq.elements!.elementAt(0) as ASN1Sequence);\n\n      // DECRYPT\n      var decryptedContent =\n          _decryptData(bagValueSeq.elements!.elementAt(1).valueBytes!, contentEncryptionAlgorithm, pwFormatted!);\n      var s = ASN1Sequence.fromBytes(decryptedContent);\n\n      //private key\n      pems.insert(\n        0,\n        X509Utils.encodeASN1ObjectToPem(s, CryptoUtils.BEGIN_PRIVATE_KEY, CryptoUtils.END_PRIVATE_KEY),\n      );\n      return;\n    }\n\n    //certificate\n    if (bagId.objectIdentifierAsString == \"1.2.840.113549.1.12.10.1.3\") {\n      var octet = ASN1OctetString.fromBytes(bagValueSeq.elements!.elementAt(1).valueBytes!);\n      var x509Seq = ASN1Sequence.fromBytes(octet.valueBytes!);\n\n      var cer = X509Utils.encodeASN1ObjectToPem(x509Seq, X509Utils.BEGIN_CERT, X509Utils.END_CERT);\n      pems.add(cer);\n      return;\n    }\n\n    // pkcs-12-keyBag\n    if (bagId.objectIdentifierAsString == \"1.2.840.113549.1.12.10.1.1\") {\n      var seq = bagValueSeq.elements!.elementAt(1) as ASN1Sequence;\n      var identifier = seq.elements!.elementAt(0) as ASN1ObjectIdentifier;\n      switch (identifier.objectIdentifierAsString!) {\n        case \"1.2.840.113549.1.1.1\": // rsaEncryption\n          pems.insert(\n            0,\n            X509Utils.encodeASN1ObjectToPem(bagValueSeq, CryptoUtils.BEGIN_PRIVATE_KEY, CryptoUtils.END_PRIVATE_KEY),\n          );\n          break;\n      }\n\n      return;\n    }\n  }\n\n  static Uint8List _decryptData(\n      Uint8List data, ASN1AlgorithmIdentifier contentEncryptionAlgorithm, Uint8List pwFormatted) {\n// GET ALGORITHM\n    var encryptionAlgorithm = _algorithmFromOi(contentEncryptionAlgorithm.algorithm.objectIdentifierAsString!);\n// GET SALT AND MACITER AND DIGEST ALGORITHM\n    Uint8List salt = _getSaltFromAlgorithmParameters(contentEncryptionAlgorithm.parameters);\n    int macIter = _getMacIterFromAlgorithmParameters(contentEncryptionAlgorithm.parameters);\n    var digestAlgorithm = _getDigestAlgorithmFromEncryptionAlgorithm(encryptionAlgorithm);\n    return _decrypt(data, encryptionAlgorithm, pwFormatted, salt, macIter, digestAlgorithm);\n  }\n\n  static String _algorithmFromOi(String keyPbe) {\n    switch (keyPbe) {\n      case '1.2.840.113549.1.12.1.6':\n        return \"PBE-SHA1-RC2-40\";\n      case '1.2.840.113549.1.12.1.5':\n        return \"PBE-SHA1-RC2-128\";\n      case '1.2.840.113549.1.12.1.2':\n        return \"PBE-SHA1-RC4-40\";\n      case '1.2.840.113549.1.12.1.1':\n        return \"PBE-SHA1-RC4-128\";\n      case '1.2.840.113549.1.12.1.4':\n        return \"PBE-SHA1-2DES\";\n      case '1.2.840.113549.1.12.1.3':\n        return \"PBE-SHA1-3DES\";\n      default:\n        throw ArgumentError('unsupported algorithm');\n    }\n  }\n\n  static String _getDigestAlgorithmFromEncryptionAlgorithm(String keyPbe) {\n    switch (keyPbe) {\n      case 'PBE-SHA1-RC2-40':\n      case 'PBE-SHA1-RC2-128':\n      case \"PBE-SHA1-RC4-40\":\n      case \"PBE-SHA1-RC4-128\":\n      case \"PBE-SHA1-2DES\":\n      case 'PBE-SHA1-3DES':\n        return \"SHA-1\";\n      default:\n        throw ArgumentError('unsupported algorithm');\n    }\n  }\n\n  static Uint8List _getSaltFromAlgorithmParameters(ASN1Object? parameters) {\n    var seq = parameters as ASN1Sequence;\n    if (seq.elements != null && seq.elements!.isNotEmpty) {\n      var asn1Octet = seq.elements!.elementAt(0) as ASN1OctetString;\n      return asn1Octet.valueBytes!;\n    }\n    return Uint8List.fromList([]);\n  }\n\n  static int _getMacIterFromAlgorithmParameters(ASN1Object? parameters) {\n    var seq = parameters as ASN1Sequence;\n    if (seq.elements != null && seq.elements!.isNotEmpty) {\n      var asn1Int = seq.elements!.elementAt(1) as ASN1Integer;\n      return asn1Int.integer!.toInt();\n    }\n    return 1;\n  }\n}\n"
  },
  {
    "path": "lib/network/util/cert/x509.dart",
    "content": "// ignore_for_file: constant_identifier_names, depend_on_referenced_packages\n\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:pointycastle/asn1/unsupported_object_identifier_exception.dart';\nimport 'package:pointycastle/pointycastle.dart';\nimport 'package:proxypin/network/util/cert/extension.dart';\n\nimport '../crypto.dart';\nimport '../lang.dart';\nimport 'basic_constraints.dart';\nimport 'cert_data.dart';\nimport 'key_usage.dart';\n\n/// @author wanghongen\n/// 2023/7/26\nclass X509Utils {\n  static const String BEGIN_CERT = '-----BEGIN CERTIFICATE-----';\n  static const String END_CERT = '-----END CERTIFICATE-----';\n\n  static const BEGIN_CRL = '-----BEGIN X509 CRL-----';\n  static const END_CRL = '-----END X509 CRL-----';\n\n  //所在国家\n  static const String COUNTRY_NAME = \"2.5.4.6\";\n  static const String SERIAL_NUMBER = \"2.5.4.5\";\n  static const String DN_QUALIFIER = \"2.5.4.46\";\n\n  ///android 系统证书名称\n  static String getSubjectHashName(Map<String, String?> subject) {\n    // Add Issuer\n    var issuerSeq = ASN1Sequence();\n    for (var k in subject.keys) {\n      var s = X509Utils._identifier(k, subject[k]!);\n      issuerSeq.add(s);\n    }\n    var derEncoded = issuerSeq.encode();\n    // Convert the hash to a long value\n    var hashBytes = md5.convert(derEncoded).bytes;\n    int hash = (hashBytes[0] & 0xff) |\n        ((hashBytes[1] & 0xff) << 8) |\n        ((hashBytes[2] & 0xff) << 16) |\n        ((hashBytes[3] & 0xff) << 24);\n    String hexString = hash.toRadixString(16).padLeft(8, '0');\n    return hexString;\n  }\n\n  ///\n  /// Encode the given [asn1Object] to PEM format and adding the [begin] and [end].\n  ///\n  static String encodeASN1ObjectToPem(ASN1Object asn1Object, String begin, String end, {String newLine = '\\n'}) {\n    var bytes = asn1Object.encode();\n    var chunks = Strings.chunk(base64.encode(bytes), 64);\n    return '$begin$newLine${chunks.join(newLine)}$newLine$end';\n  }\n\n  ///\n  /// Converts the given DER encoded CRL to a PEM string with the corresponding\n  /// headers. The given [bytes] can be taken directly from a .crl file.\n  ///\n  static String crlDerToPem(Uint8List bytes) {\n    return formatKeyString(base64.encode(bytes), BEGIN_CRL, END_CRL);\n  }\n\n  ///\n  /// Formats the given [key] by chunking the [key] and adding the [begin] and [end] to the [key].\n  ///\n  /// The line length will be defined by the given [chunkSize]. The default value is 64.\n  ///\n  /// Each line will be delimited by the given [lineDelimiter]. The default value is '\\n'.w\n  ///\n  static String formatKeyString(String key, String begin, String end,\n      {int chunkSize = 64, String lineDelimiter = '\\n'}) {\n    var sb = StringBuffer();\n    var chunks = Strings.chunk(key, chunkSize);\n    if (Strings.isNotEmpty(begin)) {\n      sb.write(begin + lineDelimiter);\n    }\n    for (var s in chunks) {\n      sb.write(s + lineDelimiter);\n    }\n    if (Strings.isNotEmpty(end)) {\n      sb.write(end);\n      return sb.toString();\n    } else {\n      var tmp = sb.toString();\n      return tmp.substring(0, tmp.lastIndexOf(lineDelimiter));\n    }\n  }\n\n  ///\n  /// Parses the given PEM to a [X509CertificateData] object.\n  ///\n  /// Throws an [ASN1Exception] if the pem could not be read by the [ASN1Parser].\n  ///\n  static X509CertificateData x509CertificateFromPem(String pem) {\n    var bytes = CryptoUtils.getBytesFromPEMString(pem);\n    var asn1Parser = ASN1Parser(bytes);\n    var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;\n\n    var x509 = _x509FromAsn1Sequence(topLevelSeq);\n\n    var sha1String = CryptoUtils.getHash(bytes, algorithmName: 'SHA-1');\n    var md5String = CryptoUtils.getHash(bytes, algorithmName: 'MD5');\n    var sha256String = CryptoUtils.getHash(bytes, algorithmName: 'SHA-256');\n\n    x509.plain = pem;\n    x509.sha1Thumbprint = sha1String;\n    x509.md5Thumbprint = md5String;\n    x509.sha256Thumbprint = sha256String;\n    return x509;\n  }\n\n  ///\n  /// Generates a self signed certificate\n  ///\n  /// * [privateKey] = The private key used for signing\n  /// * [csr] = The CSR containing the DN and public key\n  /// * [days] = The validity in days\n  /// * [sans] = Subject alternative names to place within the certificate\n  /// * [extKeyUsage] = The extended key usage definition\n  /// * [serialNumber] = The serialnumber. If not set the default will be 1.\n  /// * [issuer] = The issuer. If null, the issuer will be the subject of the given csr.\n  ///\n  static String generateSelfSignedCertificate(\n    X509CertificateData caRoot,\n    RSAPublicKey publicKey,\n    RSAPrivateKey privateKey,\n    int days, {\n    List<String>? sans,\n    String serialNumber = '1',\n    Map<String, String>? issuer,\n    Map<String, String>? subject,\n    ExtensionKeyUsage? keyUsage,\n    List<ExtendedKeyUsage>? extKeyUsage,\n    BasicConstraints? basicConstraints,\n  }) {\n    var data = ASN1Sequence();\n\n    // Add version\n    var version = ASN1Object(tag: 0xA0);\n    version.valueBytes = ASN1Integer.fromtInt(2).encode();\n    data.add(version);\n\n    // Add serial number\n    data.add(ASN1Integer(BigInt.parse(serialNumber)));\n\n    // Add protocol\n    var blockProtocol = ASN1Sequence();\n    blockProtocol.add(ASN1ObjectIdentifier.fromIdentifierString(caRoot.signatureAlgorithm));\n    blockProtocol.add(ASN1Null());\n    data.add(blockProtocol);\n\n    issuer ??= Map.from(caRoot.subject);\n\n    // Add Issuer\n    var issuerSeq = ASN1Sequence();\n    for (var k in issuer.keys) {\n      var s = _identifier(k, issuer[k]!);\n      issuerSeq.add(s);\n    }\n    data.add(issuerSeq);\n\n    // Add Validity\n    var validitySeq = ASN1Sequence();\n    validitySeq.add(ASN1UtcTime(DateTime.now().subtract(const Duration(days: 3)).toUtc()));\n    validitySeq.add(ASN1UtcTime(DateTime.now().add(Duration(days: days)).toUtc()));\n    data.add(validitySeq);\n\n    // Add Subject\n    var subjectSeq = ASN1Sequence();\n    subject ??= Map.from(caRoot.subject);\n\n    for (var k in subject.keys) {\n      var s = _identifier(k, subject[k]!);\n      subjectSeq.add(s);\n    }\n\n    data.add(subjectSeq);\n\n    // Add Public Key\n    data.add(_makePublicKeyBlock(publicKey));\n\n    // Add Extensions\n\n    if (Lists.isNotEmpty(sans) || keyUsage != null || Lists.isNotEmpty(extKeyUsage)) {\n      var extensionTopSequence = ASN1Sequence();\n\n      // Add basic constraints 2.5.29.19\n      if (basicConstraints != null) {\n        var basicConstraintsValue = ASN1Sequence();\n        basicConstraintsValue.add(ASN1Boolean(basicConstraints.isCA));\n        if (basicConstraints.pathLenConstraint != null) {\n          basicConstraintsValue.add(ASN1Integer(BigInt.from(basicConstraints.pathLenConstraint!)));\n        }\n        var octetString = ASN1OctetString(octets: basicConstraintsValue.encode());\n        var basicConstraintsSequence = ASN1Sequence();\n        basicConstraintsSequence.add(Extension.basicConstraints);\n        if (basicConstraints.critical) {\n          basicConstraintsSequence.add(ASN1Boolean(true));\n        }\n        basicConstraintsSequence.add(octetString);\n        extensionTopSequence.add(basicConstraintsSequence);\n      }\n\n      // Add key usage  2.5.29.15\n      if (keyUsage != null) {\n        extensionTopSequence.add(keyUsageSequence(keyUsage)!);\n      }\n\n      //2.5.29.17\n      if (sans != null && sans.isNotEmpty) {\n        var sanList = ASN1Sequence();\n        for (var s in sans) {\n          sanList.add(ASN1PrintableString(stringValue: s, tag: 0x82));\n        }\n        var octetString = ASN1OctetString(octets: sanList.encode());\n\n        var sanSequence = ASN1Sequence();\n        sanSequence.add(Extension.subjectAlternativeName);\n        sanSequence.add(octetString);\n        extensionTopSequence.add(sanSequence);\n      }\n\n      // Add ext key usage 2.5.29.37\n      var extKeyUsageSequence = extendedKeyUsageEncodings(extKeyUsage);\n      if (extKeyUsageSequence != null) {\n        extensionTopSequence.add(extKeyUsageSequence);\n      }\n\n      var extObj = ASN1Object(tag: 0xA3);\n      extObj.valueBytes = extensionTopSequence.encode();\n\n      data.add(extObj);\n    }\n\n    var outer = ASN1Sequence();\n    outer.add(data);\n    outer.add(blockProtocol);\n    var encode = _rsaSign(data.encode(), privateKey, _getDigestFromOi(caRoot.signatureAlgorithm));\n    outer.add(ASN1BitString(stringValues: encode));\n\n    var chunks = Strings.chunk(base64Encode(outer.encode()), 64);\n\n    return '$BEGIN_CERT\\n${chunks.join('\\r\\n')}\\n$END_CERT';\n  }\n\n  static X509CertificateData _x509FromAsn1Sequence(ASN1Sequence topLevelSeq) {\n    var tbsCertificateSeq = topLevelSeq.elements!.elementAt(0) as ASN1Sequence;\n    var signatureAlgorithmSeq = topLevelSeq.elements!.elementAt(1) as ASN1Sequence;\n    var signateureSeq = topLevelSeq.elements!.elementAt(2) as ASN1BitString;\n\n    // tbsCertificate\n    var element = 0;\n    // Version\n    var version = 1;\n    if (tbsCertificateSeq.elements!.elementAt(0) is ASN1Integer) {\n      // The version ASN1Object ist missing use version 1\n      version = 1;\n      element = -1;\n    } else {\n      // Version 1 (int = 0), version 2 (int = 1) or version 3 (int = 2)\n      var versionObject = tbsCertificateSeq.elements!.elementAt(element + 0);\n      version = versionObject.valueBytes!.elementAt(2);\n      version++;\n    }\n\n    // Serial Number\n    var serialInteger = tbsCertificateSeq.elements!.elementAt(element + 1) as ASN1Integer;\n    var serialNumber = serialInteger.integer;\n\n    // Signature\n    // var signatureSequence = tbsCertificateSeq.elements!.elementAt(element + 2) as ASN1Sequence;\n    // var o = signatureSequence.elements!.elementAt(0) as ASN1ObjectIdentifier;\n    // var signatureAlgorithm = o.objectIdentifierAsString!;\n    // var signatureAlgorithmReadable = o.readableName!;\n\n    // Issuer\n    var issuerSequence = tbsCertificateSeq.elements!.elementAt(element + 3) as ASN1Sequence;\n    var issuer = _getDnFromSeq(issuerSequence);\n\n    // Validity\n    var validitySequence = tbsCertificateSeq.elements!.elementAt(element + 4) as ASN1Sequence;\n    var validity = _getValidityFromSeq(validitySequence);\n\n    // Subject\n    var subjectSequence = tbsCertificateSeq.elements!.elementAt(element + 5) as ASN1Sequence;\n    var subject = _getDnFromSeq(subjectSequence);\n\n    // Subject Public Key Info\n    var pubKeySequence = tbsCertificateSeq.elements!.elementAt(element + 6) as ASN1Sequence;\n    var subjectPublicKeyInfo = _getSubjectPublicKeyInfoFromSeq(pubKeySequence);\n\n    X509CertificateDataExtensions? extensions;\n    if (version > 1 && tbsCertificateSeq.elements!.length > element + 7) {\n      var extensionObject = tbsCertificateSeq.elements!.elementAt(element + 7);\n      var extParser = ASN1Parser(extensionObject.valueBytes);\n      var extSequence = extParser.nextObject() as ASN1Sequence;\n      extensions = _getExtensionsFromSeq(extSequence);\n    }\n\n    // signatureAlgorithm\n    var pubKeyOid = signatureAlgorithmSeq.elements!.elementAt(0) as ASN1ObjectIdentifier;\n\n    // signatureValue\n    var sigAsString = _bytesAsString(signateureSeq.valueBytes!);\n\n    return X509CertificateData(\n      version: version,\n      serialNumber: serialNumber!,\n      signatureAlgorithm: pubKeyOid.objectIdentifierAsString!,\n      signatureAlgorithmReadableName: pubKeyOid.readableName,\n      signature: sigAsString,\n      issuer: issuer,\n      validity: validity,\n      subject: subject,\n      publicKeyData: X509CertificatePublicKeyData.fromSubjectPublicKeyInfo(subjectPublicKeyInfo),\n      subjectAlternativNames: extensions?.subjectAlternativNames,\n      extKeyUsage: extensions?.extKeyUsage,\n      extensions: extensions,\n      // tbsCertificate: tbsCertificate,\n      tbsCertificateSeqAsString: base64.encode(\n        tbsCertificateSeq.encode(),\n      ),\n    );\n  }\n\n  static X509CertificateDataExtensions _getExtensionsFromSeq(ASN1Sequence extSequence) {\n    List<String>? sans;\n    List<KeyUsage>? keyUsage;\n    List<ExtendedKeyUsage>? extKeyUsage;\n    List<dynamic> basicConstraints;\n    var extensions = X509CertificateDataExtensions();\n    for (var subseq in extSequence.elements!) {\n        var seq = subseq as ASN1Sequence;\n        var oi = seq.elements!.elementAt(0) as ASN1ObjectIdentifier;\n        if (oi.objectIdentifierAsString == '2.5.29.17') {\n          if (seq.elements!.length == 3) {\n            sans = _fetchSansFromExtension(seq.elements!.elementAt(2));\n          } else {\n            sans = _fetchSansFromExtension(seq.elements!.elementAt(1));\n          }\n          extensions.subjectAlternativNames = sans;\n        }\n\n        var keyUsageSequence = ASN1Sequence();\n        keyUsageSequence.add(ASN1ObjectIdentifier.fromIdentifierString('2.5.29.15'));\n\n        if (oi.objectIdentifierAsString == '2.5.29.15') {\n          if (seq.elements!.length == 3) {\n            keyUsage = _fetchKeyUsageFromExtension(seq.elements!.elementAt(2));\n          } else {\n            keyUsage = _fetchKeyUsageFromExtension(seq.elements!.elementAt(1));\n          }\n          extensions.keyUsage = keyUsage;\n        }\n        if (oi.objectIdentifierAsString == '2.5.29.37') {\n          if (seq.elements!.length == 3) {\n            extKeyUsage = _fetchExtendedKeyUsageFromExtension(seq.elements!.elementAt(2));\n          } else {\n            extKeyUsage = _fetchExtendedKeyUsageFromExtension(seq.elements!.elementAt(1));\n          }\n          extensions.extKeyUsage = extKeyUsage;\n        }\n        if (oi.objectIdentifierAsString == '2.5.29.19') {\n          if (seq.elements!.length == 3) {\n            basicConstraints = _fetchBasicConstraintsFromExtension(seq.elements!.elementAt(2));\n          } else {\n            basicConstraints = [null, null];\n          }\n\n          extensions.cA = basicConstraints[0];\n          extensions.pathLenConstraint = basicConstraints[1];\n        }\n        if (oi.objectIdentifierAsString == '1.3.6.1.5.5.7.1.12') {\n          var vmcData = _fetchVmcLogo(seq.elements!.elementAt(1));\n          extensions.vmc = vmcData;\n        }\n        if (oi.objectIdentifierAsString == '2.5.29.31') {\n          var cRLDistributionPoints = _fetchCrlDistributionPoints(seq.elements!.elementAt(1));\n          extensions.cRLDistributionPoints = cRLDistributionPoints;\n        }\n      }\n    return extensions;\n  }\n\n  static ASN1Sequence? keyUsageSequence(ExtensionKeyUsage keyUsages) {\n    var octetString = ASN1OctetString(octets: keyUsages.bitString.encode());\n\n    var keyUsageSequence = ASN1Sequence();\n    keyUsageSequence.add(Extension.keyUsage);\n    if (keyUsages.critical) {\n      keyUsageSequence.add(ASN1Boolean(true));\n    }\n    keyUsageSequence.add(octetString);\n\n    return keyUsageSequence;\n  }\n\n  static ASN1Sequence? extendedKeyUsageEncodings(List<ExtendedKeyUsage>? extKeyUsage) {\n    if (extKeyUsage == null || extKeyUsage.isEmpty) {\n      return null;\n    }\n    var extKeyUsageList = ASN1Sequence();\n    for (var s in extKeyUsage) {\n      var oi = <int>[];\n      switch (s) {\n        case ExtendedKeyUsage.SERVER_AUTH:\n          oi = [1, 3, 6, 1, 5, 5, 7, 3, 1];\n          break;\n        case ExtendedKeyUsage.CLIENT_AUTH:\n          oi = [1, 3, 6, 1, 5, 5, 7, 3, 2];\n          break;\n        case ExtendedKeyUsage.CODE_SIGNING:\n          oi = [1, 3, 6, 1, 5, 5, 7, 3, 3];\n          break;\n        case ExtendedKeyUsage.EMAIL_PROTECTION:\n          oi = [1, 3, 6, 1, 5, 5, 7, 3, 4];\n          break;\n        case ExtendedKeyUsage.TIME_STAMPING:\n          oi = [1, 3, 6, 1, 5, 5, 7, 3, 8];\n          break;\n        case ExtendedKeyUsage.OCSP_SIGNING:\n          oi = [1, 3, 6, 1, 5, 5, 7, 3, 9];\n          break;\n        case ExtendedKeyUsage.BIMI:\n          oi = [1, 3, 6, 1, 5, 5, 7, 3, 31];\n          break;\n      }\n\n      extKeyUsageList.add(ASN1ObjectIdentifier(oi));\n    }\n\n    var octetString = ASN1OctetString(octets: extKeyUsageList.encode());\n\n    var extKeyUsageSequence = ASN1Sequence();\n    extKeyUsageSequence.add(Extension.extendedKeyUsage);\n    extKeyUsageSequence.add(octetString);\n    return extKeyUsageSequence;\n  }\n\n  static SubjectPublicKeyInfo _getSubjectPublicKeyInfoFromSeq(ASN1Sequence pubKeySequence) {\n    var algSeq = pubKeySequence.elements!.elementAt(0) as ASN1Sequence;\n    var algOi = algSeq.elements!.elementAt(0) as ASN1ObjectIdentifier;\n    var asn1AlgParameters = algSeq.elements!.elementAt(1);\n    var algParameters = '';\n    var algParametersReadable = '';\n    if (asn1AlgParameters is ASN1ObjectIdentifier) {\n      algParameters = asn1AlgParameters.objectIdentifierAsString!;\n      algParametersReadable = asn1AlgParameters.readableName!;\n    }\n\n    var pubBitString = pubKeySequence.elements!.elementAt(1) as ASN1BitString;\n    var asn1PubKeyParser = ASN1Parser(pubBitString.stringValues as Uint8List?);\n    ASN1Object? next;\n    try {\n      next = asn1PubKeyParser.nextObject();\n    } catch (e) {\n      // continue\n    }\n    int pubKeyLength;\n    int? exponent;\n    var pubKeyAsBytes = pubKeySequence.encodedBytes;\n    if (next != null && next is ASN1Sequence) {\n      var s = next;\n      var key = s.elements!.elementAt(0) as ASN1Integer;\n      if (s.elements!.length == 2 && s.elements!.elementAt(1) is ASN1Integer) {\n        var asn1Exponent = s.elements!.elementAt(1) as ASN1Integer;\n        exponent = asn1Exponent.integer!.toInt();\n      }\n      pubKeyLength = key.integer!.bitLength;\n      //pubKeyAsBytes = s.encodedBytes;\n    } else {\n      //pubKeyAsBytes = pubBitString.valueBytes;\n      var length = pubBitString.valueBytes!.elementAt(0) == 0\n          ? (pubBitString.valueByteLength! - 1)\n          : pubBitString.valueByteLength;\n      pubKeyLength = length! * 8;\n    }\n\n    var pubKeyThumbprint = CryptoUtils.getHash(pubKeySequence.encodedBytes!, algorithmName: 'SHA-1');\n    var pubKeySha256Thumbprint = CryptoUtils.getHash(pubKeySequence.encodedBytes!, algorithmName: 'SHA-256');\n\n    return SubjectPublicKeyInfo(\n      algorithm: algOi.objectIdentifierAsString,\n      algorithmReadableName: algOi.readableName,\n      parameter: algParameters != '' ? algParameters : null,\n      parameterReadableName: algParametersReadable != '' ? algParametersReadable : null,\n      length: pubKeyLength,\n      bytes: _bytesAsString(pubKeyAsBytes!),\n      sha1Thumbprint: pubKeyThumbprint,\n      sha256Thumbprint: pubKeySha256Thumbprint,\n      exponent: exponent,\n    );\n  }\n\n  ///\n  /// Converts the bytes to a hex string\n  ///\n  static String _bytesAsString(Uint8List bytes) {\n    var b = StringBuffer();\n    for (var v in bytes) {\n      var s = v.toRadixString(16);\n      if (s.length == 1) {\n        b.write('0$s');\n      } else {\n        b.write(s);\n      }\n    }\n    return b.toString().toUpperCase();\n  }\n\n  ///\n  /// Fetches the base64 encoded VMC logo from the given [extData]\n  ///\n  static VmcData _fetchVmcLogo(ASN1Object extData) {\n    var octet = extData as ASN1OctetString;\n    var vmcParser = ASN1Parser(octet.valueBytes);\n    var topSeq = vmcParser.nextObject() as ASN1Sequence;\n    var obj1 = topSeq.elements!.elementAt(0);\n    var obj1Parser = ASN1Parser(obj1.valueBytes);\n    var obj2 = obj1Parser.nextObject();\n    var obj2Parser = ASN1Parser(obj2.valueBytes);\n    var obj2Seq = obj2Parser.nextObject() as ASN1Sequence;\n    var nextSeq = obj2Seq.elements!.elementAt(0) as ASN1Sequence;\n    var finalSeq = nextSeq.elements!.elementAt(0) as ASN1Sequence;\n\n    var data = VmcData();\n    // Parse fileType\n    var ia5 = finalSeq.elements!.elementAt(0) as ASN1IA5String;\n    var fileType = ia5.stringValue!;\n\n    // Parse hash\n    var hashSeq = finalSeq.elements!.elementAt(1) as ASN1Sequence;\n    var hasFinalSeq = hashSeq.elements!.elementAt(0) as ASN1Sequence;\n    var algSeq = hasFinalSeq.elements!.elementAt(0) as ASN1Sequence;\n    var oi = algSeq.elements!.elementAt(0) as ASN1ObjectIdentifier;\n    data.hashAlgorithm = oi.objectIdentifierAsString;\n    data.hashAlgorithmReadable = oi.readableName;\n    var octetString = hasFinalSeq.elements!.elementAt(1) as ASN1OctetString;\n    var hash = _bytesAsString(octetString.octets!);\n    data.hash = hash;\n\n    // Parse base64 logo\n    var logoSeq = finalSeq.elements!.elementAt(2) as ASN1Sequence;\n    var ia5Logo = logoSeq.elements!.elementAt(0) as ASN1IA5String;\n    var base64LogoGzip = ia5Logo.stringValue;\n    var gzip = base64LogoGzip!.substring(base64LogoGzip.indexOf(',') + 1);\n    final decodedData = GZipCodec().decode(base64.decode(gzip));\n    var base64Logo = base64.encode(decodedData);\n\n    data.base64Logo = base64Logo;\n    data.type = fileType;\n\n    return data;\n  }\n\n  ///\n  /// Parses the given object identifier values to the internal enum\n  ///\n  static List<KeyUsage> _fetchKeyUsageFromExtension(ASN1Object extData) {\n    var keyUsage = <KeyUsage>[];\n    var octet = extData as ASN1OctetString;\n    var keyUsageParser = ASN1Parser(octet.valueBytes);\n    var keyUsageBitString = keyUsageParser.nextObject() as ASN1BitString;\n    if (keyUsageBitString.valueBytes?.isEmpty ?? true) {\n      return keyUsage;\n    }\n\n    final Uint8List bytes = keyUsageBitString.valueBytes!;\n    final int lastBitsToSkip = bytes.first;\n    final int amountOfBytes = bytes.length - 1; //don't count the first byte\n\n    for (int bitCounter = 0; bitCounter < amountOfBytes * 8 - lastBitsToSkip; ++bitCounter) {\n      final int byteIndex = bitCounter ~/ 8; // the current byte\n      final int bitIndex = bitCounter % 8; // the current bit\n      if (byteIndex >= amountOfBytes) {\n        return keyUsage;\n      }\n\n      final int byte = bytes[1 + byteIndex]; //skip the first byte\n      final bool keyBit = _getBitOfByte(byte, bitIndex);\n\n      if (keyBit == true && KeyUsage.values.length > bitCounter) {\n        keyUsage.add(KeyUsage.values[bitCounter]);\n      }\n    }\n    return keyUsage;\n  }\n\n  /// From left to right. Returns [true] for 1 and [false] for [0].\n  static bool _getBitOfByte(int byte, int bitIndex) {\n    final int shift = 7 - bitIndex;\n    final int shiftedByte = byte >> shift;\n    if (shiftedByte & 1 == 1) {\n      return true;\n    } else {\n      return false;\n    }\n  }\n\n  ///\n  /// Parses the given object identifier values to the internal enum\n  ///\n  static List<ExtendedKeyUsage> _fetchExtendedKeyUsageFromExtension(ASN1Object extData) {\n    var extKeyUsage = <ExtendedKeyUsage>[];\n    var octet = extData as ASN1OctetString;\n    var keyUsageParser = ASN1Parser(octet.valueBytes);\n    var keyUsageSeq = keyUsageParser.nextObject() as ASN1Sequence;\n    for (var oi in keyUsageSeq.elements!) {\n      if (oi is ASN1ObjectIdentifier) {\n        var s = oi.objectIdentifierAsString;\n        switch (s) {\n          case '1.3.6.1.5.5.7.3.1':\n            extKeyUsage.add(ExtendedKeyUsage.SERVER_AUTH);\n            break;\n          case '1.3.6.1.5.5.7.3.2':\n            extKeyUsage.add(ExtendedKeyUsage.CLIENT_AUTH);\n            break;\n          case '1.3.6.1.5.5.7.3.3':\n            extKeyUsage.add(ExtendedKeyUsage.CODE_SIGNING);\n            break;\n          case '1.3.6.1.5.5.7.3.4':\n            extKeyUsage.add(ExtendedKeyUsage.EMAIL_PROTECTION);\n            break;\n          case '1.3.6.1.5.5.7.3.8':\n            extKeyUsage.add(ExtendedKeyUsage.TIME_STAMPING);\n            break;\n          case '1.3.6.1.5.5.7.3.9':\n            extKeyUsage.add(ExtendedKeyUsage.OCSP_SIGNING);\n            break;\n          case '1.3.6.1.5.5.7.3.31':\n            extKeyUsage.add(ExtendedKeyUsage.BIMI);\n            break;\n          default:\n        }\n      }\n    }\n    return extKeyUsage;\n  }\n\n  ///\n  /// Parses the given ASN1Object to the two basic constraint\n  /// fields cA and pathLenConstraint. Returns a list of types [bool, int] if\n  /// cA is true and a valid pathLenConstraint is specified, else the\n  /// corresponding element will be null.\n  ///\n  static List<dynamic> _fetchBasicConstraintsFromExtension(ASN1Object extData) {\n    var basicConstraints = <dynamic>[null, null];\n    var octet = extData as ASN1OctetString;\n    var constraintParser = ASN1Parser(octet.valueBytes);\n    var constraintSeq = constraintParser.nextObject() as ASN1Sequence;\n    for (var obj in constraintSeq.elements!) {\n      if (obj is ASN1Boolean) {\n        basicConstraints[0] = obj.boolValue;\n      }\n      if (obj is ASN1Integer) {\n        basicConstraints[1] = obj.integer!.toInt();\n      }\n    }\n    return basicConstraints;\n  }\n\n  ///\n  /// Fetches a list of subject alternative names from the given [extData]\n  ///\n  static List<String> _fetchSansFromExtension(ASN1Object extData) {\n    var sans = <String>[];\n    var octet = extData as ASN1OctetString;\n    var sanParser = ASN1Parser(octet.valueBytes);\n    var sanSeq = sanParser.nextObject() as ASN1Sequence;\n    for (var san in sanSeq.elements!) {\n      if (san.tag == 135) {\n        var sb = StringBuffer();\n        if (san.valueByteLength == 16) {\n          //IPv6\n          for (var i = 0; i < (san.valueByteLength ?? 0); i++) {\n            if (sb.isNotEmpty && i % 2 == 0) {\n              sb.write(':');\n            }\n            sb.write(san.valueBytes![i].toRadixString(16).padLeft(2, '0'));\n          }\n        } else {\n          //IPv4 and others\n          for (var b in san.valueBytes!) {\n            if (sb.isNotEmpty) {\n              sb.write('.');\n            }\n            sb.write(b);\n          }\n        }\n        sans.add(sb.toString());\n      } else if (san.tag == 164) {\n        // WE HAVE CONSTRUCTED SAN\n        var constructedParser = ASN1Parser(san.valueBytes);\n        var seq = constructedParser.nextObject() as ASN1Sequence;\n        var sanValue = 'DirName:';\n        for (var san in seq.elements!) {\n          var set = san as ASN1Set;\n          var seq = set.elements!.elementAt(0) as ASN1Sequence;\n          var oid = seq.elements!.elementAt(0) as ASN1ObjectIdentifier;\n          var object = seq.elements!.elementAt(1);\n          var value = '';\n          sanValue = '$sanValue/';\n          if (object is ASN1UTF8String) {\n            var objectAsUtf8 = object;\n            value = objectAsUtf8.utf8StringValue!;\n          } else if (object is ASN1PrintableString) {\n            var objectPrintable = object;\n            value = objectPrintable.stringValue!;\n          }\n          sanValue = '$sanValue${oid.readableName}=$value';\n        }\n        sans.add(sanValue);\n      } else {\n        var s = String.fromCharCodes(san.valueBytes!);\n        sans.add(s);\n      }\n    }\n    return sans;\n  }\n\n  static List<String> _fetchCrlDistributionPoints(ASN1Object extData) {\n    var cRLDistributionPoints = <String>[];\n\n    var octet = extData as ASN1OctetString;\n    var parser = ASN1Parser(octet.valueBytes);\n    var topSeq = parser.nextObject() as ASN1Sequence;\n    for (var e in topSeq.elements!) {\n      var seq = e as ASN1Sequence;\n      var o1 = seq.elements!.elementAt(0);\n      var parser = ASN1Parser(o1.valueBytes);\n      var o2 = parser.nextObject();\n      parser = ASN1Parser(o2.valueBytes);\n      var o3 = parser.nextObject();\n      var point = String.fromCharCodes(o3.valueBytes!.toList());\n      cRLDistributionPoints.add(point);\n    }\n    return cRLDistributionPoints;\n  }\n\n  static X509CertificateValidity _getValidityFromSeq(ASN1Sequence validitySequence) {\n    DateTime? asn1FromDateTime;\n    DateTime? asn1ToDateTime;\n    if (validitySequence.elements!.elementAt(0) is ASN1UtcTime) {\n      var asn1From = validitySequence.elements!.elementAt(0) as ASN1UtcTime;\n      asn1FromDateTime = asn1From.time;\n    } else {\n      var asn1From = validitySequence.elements!.elementAt(0) as ASN1GeneralizedTime;\n      asn1FromDateTime = asn1From.dateTimeValue;\n    }\n    if (validitySequence.elements!.elementAt(1) is ASN1UtcTime) {\n      var asn1To = validitySequence.elements!.elementAt(1) as ASN1UtcTime;\n      asn1ToDateTime = asn1To.time;\n    } else {\n      var asn1To = validitySequence.elements!.elementAt(1) as ASN1GeneralizedTime;\n      asn1ToDateTime = asn1To.dateTimeValue;\n    }\n\n    return X509CertificateValidity(\n      notBefore: asn1FromDateTime!,\n      notAfter: asn1ToDateTime!,\n    );\n  }\n\n  static Map<String, String> _getDnFromSeq(ASN1Sequence issuerSequence) {\n    var dnData = <String, String>{};\n    for (var s in issuerSequence.elements as dynamic) {\n      for (var ele in s.elements!) {\n        var seq = ele as ASN1Sequence;\n        var o = seq.elements!.elementAt(0) as ASN1ObjectIdentifier;\n        var object = seq.elements!.elementAt(1);\n        String? value = '';\n        if (object is ASN1UTF8String) {\n          var objectAsUtf8 = object;\n          value = objectAsUtf8.utf8StringValue;\n        } else if (object is ASN1PrintableString) {\n          var objectPrintable = object;\n          value = objectPrintable.stringValue;\n        } else if (object is ASN1TeletextString) {\n          var objectTeletext = object;\n          value = objectTeletext.stringValue;\n        }\n        dnData.putIfAbsent(o.objectIdentifierAsString!, () => value ?? '');\n      }\n    }\n    return dnData;\n  }\n\n  static ASN1Set _identifier(String k, String value) {\n    ASN1ObjectIdentifier oIdentifier;\n    try {\n      oIdentifier = ASN1ObjectIdentifier.fromName(k);\n    } on UnsupportedObjectIdentifierException {\n      oIdentifier = ASN1ObjectIdentifier.fromIdentifierString(k);\n    }\n\n    ASN1Object pString;\n    var identifier = oIdentifier.objectIdentifierAsString;\n    if (identifier == COUNTRY_NAME || SERIAL_NUMBER == identifier || identifier == DN_QUALIFIER) {\n      pString = ASN1PrintableString(stringValue: value);\n    } else {\n      pString = ASN1UTF8String(utf8StringValue: value);\n    }\n\n    var innerSequence = ASN1Sequence(elements: [oIdentifier, pString]);\n    return ASN1Set(elements: [innerSequence]);\n  }\n\n  static Uint8List _rsaSign(Uint8List inBytes, RSAPrivateKey privateKey, String signingAlgorithm) {\n    var signer = Signer('$signingAlgorithm/RSA');\n    signer.init(true, PrivateKeyParameter<RSAPrivateKey>(privateKey));\n\n    var signature = signer.generateSignature(inBytes) as RSASignature;\n\n    return signature.bytes;\n  }\n\n  ///\n  /// Create  the public key ASN1Sequence for the csr.\n  ///\n  static ASN1Sequence _makePublicKeyBlock(RSAPublicKey publicKey) {\n    var blockEncryptionType = ASN1Sequence();\n    blockEncryptionType.add(ASN1ObjectIdentifier.fromName('rsaEncryption'));\n    blockEncryptionType.add(ASN1Null());\n\n    var publicKeySequence = ASN1Sequence();\n    publicKeySequence.add(ASN1Integer(publicKey.modulus));\n    publicKeySequence.add(ASN1Integer(publicKey.exponent));\n\n    var blockPublicKey = ASN1BitString(stringValues: publicKeySequence.encode());\n\n    var outer = ASN1Sequence();\n    outer.add(blockEncryptionType);\n    outer.add(blockPublicKey);\n\n    return outer;\n  }\n\n  static String _getDigestFromOi(String oi) {\n    switch (oi) {\n      case 'ecdsaWithSHA1':\n      case 'sha1WithRSAEncryption':\n        return 'SHA-1';\n      case 'ecdsaWithSHA224':\n      case 'sha224WithRSAEncryption':\n        return 'SHA-224';\n      case 'ecdsaWithSHA256':\n      case 'sha256WithRSAEncryption':\n        return 'SHA-256';\n      case 'ecdsaWithSHA384':\n      case 'sha384WithRSAEncryption':\n        return 'SHA-384';\n      case 'ecdsaWithSHA512':\n      case 'sha512WithRSAEncryption':\n        return 'SHA-512';\n      default:\n        return 'SHA-256';\n    }\n  }\n}\n"
  },
  {
    "path": "lib/network/util/compress.dart",
    "content": "import 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:brotli/brotli.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:zstandard/zstandard.dart';\n\n///GZIP 解压缩\nList<int> gzipDecode(List<int> byteBuffer) {\n  GZipCodec gzipCodec = GZipCodec();\n  try {\n    return gzipCodec.decode(byteBuffer);\n  } catch (e) {\n    logger.e(\"gzipDecode error: $e\");\n    return byteBuffer;\n  }\n}\n\n///GZIP 压缩\nList<int> gzipEncode(List<int> input) {\n  return GZipCodec().encode(input);\n}\n\n///br 解压缩\nList<int> brDecode(List<int> byteBuffer) {\n  try {\n    return brotli.decode(byteBuffer);\n  } catch (e) {\n    logger.e(\"brDecode error: $e\");\n    return byteBuffer;\n  }\n}\n\n///zstd 解压缩\nFuture<List<int>?> zstdDecode(List<int> byteBuffer) async {\n  final zstandard = Zstandard();\n  try {\n    return zstandard.decompress(Uint8List.fromList(byteBuffer));\n  } catch (e) {\n    logger.e(\"zstdDecode error: $e\");\n    return byteBuffer;\n  }\n}\n\n\n///zlib\nList<int> zlibDecode(List<int> byteBuffer) {\n  try {\n    final rawDeflateDecoder = ZLibDecoder(raw: true);\n    return rawDeflateDecoder.convert(byteBuffer);\n  } catch (e) {\n    logger.e(\"zlibDecode error: $e\");\n    return byteBuffer;\n  }\n}"
  },
  {
    "path": "lib/network/util/crts.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:async';\nimport 'dart:core';\nimport 'dart:io';\nimport 'dart:math';\nimport 'dart:typed_data';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:pointycastle/export.dart';\nimport 'package:proxypin/network/util/cert/pkcs12.dart';\nimport 'package:proxypin/network/util/cert/x509.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/random.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nimport 'cache.dart';\nimport 'cert/basic_constraints.dart';\nimport 'cert/cert_data.dart';\nimport 'cert/extension.dart';\nimport 'cert/key_usage.dart';\nimport 'crypto.dart';\nimport 'file_read.dart';\n\nFuture<void> main() async {\n  await CertificateManager.getCertificateContext('www.jianshu.com');\n}\n\nenum StartState { uninitialized, initializing, initialized }\n\nclass CertificateManager {\n  /// 证书缓存\n  static final ExpiringCache<String, SecurityContext> _certificateMap =\n      ExpiringCache<String, SecurityContext>(const Duration(minutes: 15));\n\n  /// 服务端密钥\n  static AsymmetricKeyPair _serverKeyPair = CryptoUtils.generateRSAKeyPair();\n\n  /// ca证书\n  static X509CertificateData? _caCert;\n\n  /// ca私钥\n  static late RSAPrivateKey _caPriKey;\n\n  /// 是否初始化\n  static StartState _state = StartState.uninitialized;\n  static Completer<void> _initializationCompleter = Completer<void>();\n\n  static SecurityContext? get(String host) {\n    return _certificateMap[host];\n  }\n\n  static X509CertificateData? get caCert => _caCert;\n\n  /// 清除缓存\n  static void cleanCache() {\n    _certificateMap.clear();\n  }\n\n  /// 获取域名自签名证书\n  static Future<SecurityContext> getCertificateContext(String host) async {\n    SecurityContext? securityContext = _certificateMap[host];\n    if (securityContext != null) {\n      return securityContext;\n    }\n\n    if (_state != StartState.initialized) {\n      await initCAConfig();\n    }\n\n    String cer = generate(_caCert!, _serverKeyPair.publicKey as RSAPublicKey, _caPriKey, host);\n\n    var rsaPrivateKey = _serverKeyPair.privateKey as RSAPrivateKey;\n\n    securityContext = SecurityContext(withTrustedRoots: true)\n      ..useCertificateChainBytes(cer.codeUnits)\n      ..allowLegacyUnsafeRenegotiation = true\n      ..usePrivateKeyBytes(CryptoUtils.encodeRSAPrivateKeyToPemPkcs1(rsaPrivateKey).codeUnits);\n\n    _certificateMap[host] = securityContext;\n\n    return securityContext;\n  }\n\n  /// 生成域名证书 PEM（仅证书，不含私钥）\n  static Future<String> generateLeafCertificatePem(String host) async {\n    if (_state != StartState.initialized) {\n      await initCAConfig();\n    }\n    return generate(_caCert!, _serverKeyPair.publicKey as RSAPublicKey, _caPriKey, host);\n  }\n\n  /// 生成证书\n  static String generate(X509CertificateData caRoot, RSAPublicKey serverPubKey, RSAPrivateKey caPriKey, String host) {\n    //根据CA证书subject来动态生成目标服务器证书的issuer和subject\n    Map<String, String> x509Subject = {\n      'C': 'CN',\n      'ST': 'BJ',\n      'L': 'Beijing',\n      'O': 'Proxy',\n      'OU': 'ProxyPin',\n    };\n\n    x509Subject['CN'] = host;\n\n    var csrPem = X509Utils.generateSelfSignedCertificate(caRoot, serverPubKey, caPriKey, 365,\n        sans: [host], serialNumber: Random().nextInt(1000000).toString(), subject: x509Subject);\n    return csrPem;\n  }\n\n  /// 获取证书主题hash\n  static Future<String> systemCertificateName() async {\n    if (_state != StartState.initialized) {\n      await initCAConfig();\n    }\n\n    var subject = caCert!.subject;\n    return '${X509Utils.getSubjectHashName(subject)}.0';\n  }\n\n  //重新生成根证书\n  static Future<void> generateNewRootCA() async {\n    if (_state != StartState.initialized) {\n      await initCAConfig();\n    }\n\n    var generateRSAKeyPair = CryptoUtils.generateRSAKeyPair();\n    var serverPubKey = generateRSAKeyPair.publicKey as RSAPublicKey;\n    var serverPriKey = generateRSAKeyPair.privateKey as RSAPrivateKey;\n\n    //根据CA证书subject来动态生成目标服务器证书的issuer和subject\n    Map<String, String> x509Subject = {\n      'C': 'CN',\n      'ST': 'BJ',\n      'L': 'Beijing',\n      'O': 'Proxy',\n      'OU': 'ProxyPin',\n    };\n    x509Subject['CN'] = 'ProxyPin CA (${DateTime.now().dateFormat()},${RandomUtil.randomString(6).toUpperCase()})';\n\n    var csrPem = X509Utils.generateSelfSignedCertificate(\n      _caCert!,\n      serverPubKey,\n      serverPriKey,\n      825,\n      sans: [x509Subject['CN']!],\n      serialNumber: DateTime.now().millisecondsSinceEpoch.toString(),\n      issuer: x509Subject,\n      subject: x509Subject,\n      keyUsage: ExtensionKeyUsage(ExtensionKeyUsage.keyCertSign),\n      extKeyUsage: [ExtendedKeyUsage.SERVER_AUTH],\n      basicConstraints: BasicConstraints(isCA: true),\n    );\n\n    //重新写入根证书\n    var caFile = await certificateFile();\n    await caFile.writeAsString(csrPem);\n\n    //私钥\n    var serverPriKeyPem = CryptoUtils.encodeRSAPrivateKeyToPem(serverPriKey);\n    var keyFile = await privateKeyFile();\n    await keyFile.writeAsString(serverPriKeyPem);\n    cleanCache();\n    _state = StartState.uninitialized;\n  }\n\n  ///重置默认根证书\n  static Future<void> resetDefaultRootCA() async {\n    var caFile = await certificateFile();\n    await caFile.delete();\n\n    var keyFile = await privateKeyFile();\n    await keyFile.delete();\n    cleanCache();\n    _state = StartState.uninitialized;\n    initCAConfig();\n  }\n\n  static Future<void> initCAConfig() async {\n    if (_state == StartState.initialized || _state == StartState.initializing) {\n      return _initializationCompleter.future;\n    }\n\n    var startTime = DateTime.now().millisecondsSinceEpoch;\n\n    _state = StartState.initializing;\n    _initializationCompleter = Completer<void>();\n\n    try {\n      _serverKeyPair = CryptoUtils.generateRSAKeyPair();\n\n      //从项目目录加入ca根证书\n      var caPemFile = await certificateFile();\n      _caCert = X509Utils.x509CertificateFromPem(await caPemFile.readAsString());\n      //根据CA证书subject来动态生成目标服务器证书的issuer和subject\n\n      //从项目目录加入ca私钥\n      var keyFile = await privateKeyFile();\n      _caPriKey = CryptoUtils.rsaPrivateKeyFromPem(await keyFile.readAsString());\n\n      _state = StartState.initialized;\n      _initializationCompleter.complete();\n    } catch (e) {\n      logger.e('init ca config error:$e');\n      _state = StartState.uninitialized;\n      _initializationCompleter.completeError(e);\n    }\n\n    logger.d('init ca config end cost:${DateTime.now().millisecondsSinceEpoch - startTime}');\n\n    return _initializationCompleter.future;\n  }\n\n  /// 证书文件\n  static Future<File> certificateFile() async {\n    final String appPath = await getApplicationSupportDirectory().then((value) => value.path);\n    var caFile = File(\"$appPath${Platform.pathSeparator}ca.crt\");\n    if (!(await caFile.exists())) {\n      var body = await FileRead.read('assets/certs/ca.crt');\n      await caFile.writeAsBytes(body.buffer.asUint8List());\n    }\n\n    return caFile;\n  }\n\n  ///证书pem格式内容\n  static Future<String> certificatePem() async {\n    var caFile = await certificateFile();\n    return caFile.readAsString();\n  }\n\n  /// 私钥文件\n  static Future<File> privateKeyFile() async {\n    final String appPath = await getApplicationSupportDirectory().then((value) => value.path);\n    var caFile = File(\"$appPath${Platform.pathSeparator}ca_key.pem\");\n    if (!(await caFile.exists())) {\n      var body = await FileRead.read('assets/certs/ca_key.pem');\n      await caFile.writeAsBytes(body.buffer.asUint8List());\n    }\n\n    return caFile;\n  }\n\n  ///生成 p12文件\n  static Future<Uint8List> generatePkcs12(String? password) async {\n    var caFile = await CertificateManager.certificateFile();\n    var keyFile = await CertificateManager.privateKeyFile();\n    return Pkcs12.generatePkcs12(await keyFile.readAsString(), [await caFile.readAsString()], password: password);\n  }\n\n  ///import p12文件\n  static Future<void> importPkcs12(Uint8List pkcs12, String? password) async {\n    var decodePkcs12 = Pkcs12.parsePkcs12(pkcs12, password: password);\n\n    var caFile = await CertificateManager.certificateFile();\n    var keyFile = await CertificateManager.privateKeyFile();\n    if (decodePkcs12.length != 2) {\n      throw Exception('Invalid pkcs12 file');\n    }\n\n    await keyFile.writeAsString(decodePkcs12[0]);\n    await caFile.writeAsString(decodePkcs12[1]);\n\n    cleanCache();\n    _state = StartState.uninitialized;\n    initCAConfig();\n  }\n\n  /// 获取证书详细信息\n  static Future<X509CertificateData> getCertificateDetails() async {\n    if (_state != StartState.initialized) {\n      await initCAConfig();\n    }\n    return caCert!;\n  }\n}\n"
  },
  {
    "path": "lib/network/util/crypto.dart",
    "content": "import 'dart:convert';\nimport 'dart:math';\nimport 'dart:typed_data';\n\nimport 'package:pointycastle/api.dart';\nimport 'package:pointycastle/asn1/asn1_object.dart';\nimport 'package:pointycastle/asn1/asn1_parser.dart';\nimport 'package:pointycastle/asn1/primitives/asn1_bit_string.dart';\nimport 'package:pointycastle/asn1/primitives/asn1_integer.dart';\nimport 'package:pointycastle/asn1/primitives/asn1_object_identifier.dart';\nimport 'package:pointycastle/asn1/primitives/asn1_octet_string.dart';\nimport 'package:pointycastle/asn1/primitives/asn1_sequence.dart';\nimport 'package:pointycastle/asymmetric/api.dart';\nimport 'package:pointycastle/key_generators/api.dart';\nimport 'package:pointycastle/key_generators/rsa_key_generator.dart';\nimport 'package:pointycastle/paddings/pkcs7.dart';\nimport 'package:pointycastle/random/fortuna_random.dart';\n\nimport 'lang.dart';\n\nclass CryptoUtils {\n  /// ignore: constant_identifier_names\n  static const String BEGIN_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----';\n  static const String END_PUBLIC_KEY = '-----END PUBLIC KEY-----';\n\n  // ignore: constant_identifier_names\n  static const BEGIN_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----';\n  static const END_PRIVATE_KEY = '-----END PRIVATE KEY-----';\n\n  static const BEGIN_RSA_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----';\n  static const END_RSA_PRIVATE_KEY = '-----END RSA PRIVATE KEY-----';\n\n  static const BEGIN_EC_PRIVATE_KEY = '-----BEGIN EC PRIVATE KEY-----';\n  static const String END_EC_PRIVATE_KEY = '-----END EC PRIVATE KEY-----';\n\n  ///\n  /// Get a hash for the given [bytes] using the given [algorithm]\n  ///\n  /// The default [algorithm] used is **SHA-256**. All supported algorihms are :\n  ///\n  /// * SHA-1\n  /// * SHA-224\n  /// * SHA-256\n  /// * SHA-384\n  /// * SHA-512\n  /// * SHA-512/224\n  /// * SHA-512/256\n  /// * MD5\n  ///\n  static String getHash(Uint8List bytes, {String algorithmName = 'SHA-256'}) {\n    var hash = getHashPlain(bytes, algorithmName: algorithmName);\n\n    const hexDigits = '0123456789abcdef';\n    var charCodes = Uint8List(hash.length * 2);\n    for (var i = 0, j = 0; i < hash.length; i++) {\n      var byte = hash[i];\n      charCodes[j++] = hexDigits.codeUnitAt((byte >> 4) & 0xF);\n      charCodes[j++] = hexDigits.codeUnitAt(byte & 0xF);\n    }\n\n    return String.fromCharCodes(charCodes).toUpperCase();\n  }\n\n  ///\n  /// Get a hash for the given [bytes] using the given [algorithm]\n  ///\n  /// The default [algorithm] used is **SHA-256**. All supported algorihms are :\n  ///\n  /// * SHA-1\n  /// * SHA-224\n  /// * SHA-256\n  /// * SHA-384\n  /// * SHA-512\n  /// * SHA-512/224\n  /// * SHA-512/256\n  /// * MD5\n  ///\n  static Uint8List getHashPlain(Uint8List bytes, {String algorithmName = 'SHA-256'}) {\n    Uint8List hash;\n    switch (algorithmName) {\n      case 'SHA-1':\n        hash = Digest('SHA-1').process(bytes);\n        break;\n      case 'SHA-224':\n        hash = Digest('SHA-224').process(bytes);\n        break;\n      case 'SHA-256':\n        hash = Digest('SHA-256').process(bytes);\n        break;\n      case 'SHA-384':\n        hash = Digest('SHA-384').process(bytes);\n        break;\n      case 'SHA-512':\n        hash = Digest('SHA-512').process(bytes);\n        break;\n      case 'SHA-512/224':\n        hash = Digest('SHA-512/224').process(bytes);\n        break;\n      case 'SHA-512/256':\n        hash = Digest('SHA-512/256').process(bytes);\n        break;\n      case 'MD5':\n        hash = Digest('MD5').process(bytes);\n        break;\n      default:\n        throw ArgumentError('Hash not supported');\n    }\n\n    return hash;\n  }\n\n  ///\n  /// Returns the private key type of the given [pem]\n  ///\n  static String getPrivateKeyType(String pem) {\n    if (pem.startsWith(BEGIN_RSA_PRIVATE_KEY)) {\n      return 'RSA_PKCS1';\n    } else if (pem.startsWith(BEGIN_PRIVATE_KEY)) {\n      return 'RSA';\n    } else if (pem.startsWith(BEGIN_EC_PRIVATE_KEY)) {\n      return 'ECC';\n    }\n    return 'RSA';\n  }\n\n  ///\n  /// Generates a RSA [AsymmetricKeyPair] with the given [keySize].\n  /// The default value for the [keySize] is 2048 bits.\n  ///\n  /// The following keySize is supported:\n  /// * 1024\n  /// * 2048\n  /// * 3072\n  /// * 4096\n  /// * 8192\n  ///\n  static AsymmetricKeyPair generateRSAKeyPair({int keySize = 2048}) {\n    var keyParams = RSAKeyGeneratorParameters(BigInt.parse('65537'), keySize, 12);\n\n    var secureRandom = getSecureRandom();\n\n    var rngParams = ParametersWithRandom(keyParams, secureRandom);\n    var generator = RSAKeyGenerator();\n    generator.init(rngParams);\n\n    return generator.generateKeyPair();\n  }\n\n  ///\n  /// Decode a [RSAPublicKey] from the given [pem] String.\n  ///\n  static RSAPublicKey rsaPublicKeyFromPem(String pem) {\n    var bytes = CryptoUtils.getBytesFromPEMString(pem);\n    return rsaPublicKeyFromDERBytes(bytes);\n  }\n\n  ///\n  /// Decode the given [bytes] into an [RSAPublicKey].\n  ///\n  static RSAPublicKey rsaPublicKeyFromDERBytes(Uint8List bytes) {\n    var asn1Parser = ASN1Parser(bytes);\n    var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;\n    ASN1Sequence publicKeySeq;\n    if (topLevelSeq.elements![1].runtimeType == ASN1BitString) {\n      var publicKeyBitString = topLevelSeq.elements![1] as ASN1BitString;\n\n      var publicKeyAsn = ASN1Parser(publicKeyBitString.stringValues as Uint8List?);\n      publicKeySeq = publicKeyAsn.nextObject() as ASN1Sequence;\n    } else {\n      publicKeySeq = topLevelSeq;\n    }\n    var modulus = publicKeySeq.elements![0] as ASN1Integer;\n    var exponent = publicKeySeq.elements![1] as ASN1Integer;\n\n    var rsaPublicKey = RSAPublicKey(modulus.integer!, exponent.integer!);\n\n    return rsaPublicKey;\n  }\n\n  ///\n  /// Decode a [RSAPrivateKey] from the given [pem] String.\n  ///\n  static RSAPrivateKey rsaPrivateKeyFromPem(String pem) {\n    var bytes = getBytesFromPEMString(pem);\n    return rsaPrivateKeyFromDERBytes(bytes);\n  }\n\n  //\n  /// Helper function for decoding the base64 in [pem].\n  ///\n  /// Throws an ArgumentError if the given [pem] is not sourounded by begin marker -----BEGIN and\n  /// endmarker -----END or the [pem] consists of less than two lines.\n  ///\n  /// The PEM header check can be skipped by setting the optional paramter [checkHeader] to false.\n  ///\n  static Uint8List getBytesFromPEMString(String pem, {bool checkHeader = true}) {\n    var lines = LineSplitter.split(pem).map((line) => line.trim()).where((line) => line.isNotEmpty).toList();\n    String base64;\n    if (checkHeader) {\n      if (lines.length < 2 || !lines.first.startsWith('-----BEGIN') || !lines.last.startsWith('-----END')) {\n        throw ArgumentError('The given string does not have the correct '\n            'begin/end markers expected in a PEM file.');\n      }\n      base64 = lines.sublist(1, lines.length - 1).join('');\n    } else {\n      base64 = lines.join('');\n    }\n\n    return Uint8List.fromList(base64Decode(base64));\n  }\n\n  ///\n  /// Decode the given [bytes] into an [RSAPrivateKey].\n  ///\n  static RSAPrivateKey rsaPrivateKeyFromDERBytes(Uint8List bytes) {\n    var asn1Parser = ASN1Parser(bytes);\n    var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;\n    //ASN1Object version = topLevelSeq.elements[0];\n    //ASN1Object algorithm = topLevelSeq.elements[1];\n    var privateKey = topLevelSeq.elements![2];\n\n    asn1Parser = ASN1Parser(privateKey.valueBytes);\n    var pkSeq = asn1Parser.nextObject() as ASN1Sequence;\n\n    var modulus = pkSeq.elements![1] as ASN1Integer;\n    //ASN1Integer publicExponent = pkSeq.elements[2] as ASN1Integer;\n    var privateExponent = pkSeq.elements![3] as ASN1Integer;\n    var p = pkSeq.elements![4] as ASN1Integer;\n    var q = pkSeq.elements![5] as ASN1Integer;\n    //ASN1Integer exp1 = pkSeq.elements[6] as ASN1Integer;\n    //ASN1Integer exp2 = pkSeq.elements[7] as ASN1Integer;\n    //ASN1Integer co = pkSeq.elements[8] as ASN1Integer;\n\n    var rsaPrivateKey = RSAPrivateKey(modulus.integer!, privateExponent.integer!, p.integer, q.integer);\n\n    return rsaPrivateKey;\n  }\n\n  ///\n  /// Enode the given [publicKey] to PEM format using the PKCS#8 standard.\n  ///\n  static String encodeRSAPublicKeyToPem(RSAPublicKey publicKey) {\n    var algorithmSeq = ASN1Sequence();\n    var paramsAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x5, 0x0]));\n    algorithmSeq.add(ASN1ObjectIdentifier.fromName('rsaEncryption'));\n    algorithmSeq.add(paramsAsn1Obj);\n\n    var publicKeySeq = ASN1Sequence();\n    publicKeySeq.add(ASN1Integer(publicKey.modulus));\n    publicKeySeq.add(ASN1Integer(publicKey.exponent));\n    var publicKeySeqBitString = ASN1BitString(stringValues: Uint8List.fromList(publicKeySeq.encode()));\n\n    var topLevelSeq = ASN1Sequence();\n    topLevelSeq.add(algorithmSeq);\n    topLevelSeq.add(publicKeySeqBitString);\n    var dataBase64 = base64.encode(topLevelSeq.encode());\n    var chunks = Strings.chunk(dataBase64, 64);\n\n    return '$BEGIN_PUBLIC_KEY\\n${chunks.join('\\n')}\\n$END_PUBLIC_KEY';\n  }\n\n  ///\n  /// Enode the given [rsaPrivateKey] to PEM format using the PKCS#1 standard.\n  ///\n  /// The ASN1 structure is decripted at <https://tools.ietf.org/html/rfc8017#page-54>.\n  ///\n  /// ```\n  /// RSAPrivateKey ::= SEQUENCE {\n  ///   version           Version,\n  ///   modulus           INTEGER,  -- n\n  ///   publicExponent    INTEGER,  -- e\n  ///   privateExponent   INTEGER,  -- d\n  ///   prime1            INTEGER,  -- p\n  ///   prime2            INTEGER,  -- q\n  ///   exponent1         INTEGER,  -- d mod (p-1)\n  ///   exponent2         INTEGER,  -- d mod (q-1)\n  ///   coefficient       INTEGER,  -- (inverse of q) mod p\n  ///   otherPrimeInfos   OtherPrimeInfos OPTIONAL\n  /// }\n  /// ```\n  static String encodeRSAPrivateKeyToPemPkcs1(RSAPrivateKey rsaPrivateKey) {\n    var version = ASN1Integer(BigInt.from(0));\n    var modulus = ASN1Integer(rsaPrivateKey.n);\n    var publicExponent = ASN1Integer(BigInt.parse('65537'));\n    var privateExponent = ASN1Integer(rsaPrivateKey.privateExponent);\n\n    var p = ASN1Integer(rsaPrivateKey.p);\n    var q = ASN1Integer(rsaPrivateKey.q);\n    var dP = rsaPrivateKey.privateExponent! % (rsaPrivateKey.p! - BigInt.from(1));\n    var exp1 = ASN1Integer(dP);\n    var dQ = rsaPrivateKey.privateExponent! % (rsaPrivateKey.q! - BigInt.from(1));\n    var exp2 = ASN1Integer(dQ);\n    var iQ = rsaPrivateKey.q!.modInverse(rsaPrivateKey.p!);\n    var co = ASN1Integer(iQ);\n\n    var topLevelSeq = ASN1Sequence();\n    topLevelSeq.add(version);\n    topLevelSeq.add(modulus);\n    topLevelSeq.add(publicExponent);\n    topLevelSeq.add(privateExponent);\n    topLevelSeq.add(p);\n    topLevelSeq.add(q);\n    topLevelSeq.add(exp1);\n    topLevelSeq.add(exp2);\n    topLevelSeq.add(co);\n    var dataBase64 = base64.encode(topLevelSeq.encode());\n    var chunks = Strings.chunk(dataBase64, 64);\n    return '$BEGIN_RSA_PRIVATE_KEY\\n${chunks.join('\\n')}\\n$END_RSA_PRIVATE_KEY';\n  }\n\n  ///\n  /// Enode the given [rsaPrivateKey] to PEM format using the PKCS#8 standard.\n  ///\n  /// The ASN1 structure is decripted at <https://tools.ietf.org/html/rfc5208>.\n  /// ```\n  /// PrivateKeyInfo ::= SEQUENCE {\n  ///   version         Version,\n  ///   algorithm       AlgorithmIdentifier,\n  ///   PrivateKey      BIT STRING\n  /// }\n  /// ```\n  ///\n  static String encodeRSAPrivateKeyToPem(RSAPrivateKey rsaPrivateKey) {\n    var version = ASN1Integer(BigInt.from(0));\n\n    var algorithmSeq = ASN1Sequence();\n    var algorithmAsn1Obj =\n        ASN1Object.fromBytes(Uint8List.fromList([0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1]));\n    var paramsAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x5, 0x0]));\n    algorithmSeq.add(algorithmAsn1Obj);\n    algorithmSeq.add(paramsAsn1Obj);\n\n    var privateKeySeq = ASN1Sequence();\n    var modulus = ASN1Integer(rsaPrivateKey.n);\n    var publicExponent = ASN1Integer(BigInt.parse('65537'));\n    var privateExponent = ASN1Integer(rsaPrivateKey.privateExponent);\n    var p = ASN1Integer(rsaPrivateKey.p);\n    var q = ASN1Integer(rsaPrivateKey.q);\n    var dP = rsaPrivateKey.privateExponent! % (rsaPrivateKey.p! - BigInt.from(1));\n    var exp1 = ASN1Integer(dP);\n    var dQ = rsaPrivateKey.privateExponent! % (rsaPrivateKey.q! - BigInt.from(1));\n    var exp2 = ASN1Integer(dQ);\n    var iQ = rsaPrivateKey.q!.modInverse(rsaPrivateKey.p!);\n    var co = ASN1Integer(iQ);\n\n    privateKeySeq.add(version);\n    privateKeySeq.add(modulus);\n    privateKeySeq.add(publicExponent);\n    privateKeySeq.add(privateExponent);\n    privateKeySeq.add(p);\n    privateKeySeq.add(q);\n    privateKeySeq.add(exp1);\n    privateKeySeq.add(exp2);\n    privateKeySeq.add(co);\n    var publicKeySeqOctetString = ASN1OctetString(octets: Uint8List.fromList(privateKeySeq.encode()));\n\n    var topLevelSeq = ASN1Sequence();\n    topLevelSeq.add(version);\n    topLevelSeq.add(algorithmSeq);\n    topLevelSeq.add(publicKeySeqOctetString);\n    var dataBase64 = base64.encode(topLevelSeq.encode());\n    var chunks = Strings.chunk(dataBase64, 64);\n    return '$BEGIN_PRIVATE_KEY\\n${chunks.join('\\n')}\\n$END_PRIVATE_KEY';\n  }\n\n  ///\n  /// Generates a secure [FortunaRandom]\n  ///\n  static SecureRandom getSecureRandom() {\n    var secureRandom = FortunaRandom();\n    var random = Random.secure();\n    var seeds = List<int>.generate(32, (_) => random.nextInt(256));\n    secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));\n    return secureRandom;\n  }\n\n  ///\n  /// Revomes the PKCS7 / PKCS5 padding from the [padded] bytes\n  ///\n  static Uint8List removePKCS7Padding(Uint8List padded) =>\n      padded.sublist(0, padded.length - PKCS7Padding().padCount(padded));\n\n  ///\n  /// Adds a PKCS7 / PKCS5 padding to the given [bytes] and [blockSizeBytes]\n  ///\n  static Uint8List addPKCS7Padding(Uint8List bytes, int blockSizeBytes) {\n    final padLength = blockSizeBytes - (bytes.length % blockSizeBytes);\n\n    final padded = Uint8List(bytes.length + padLength)..setAll(0, bytes);\n    PKCS7Padding().addPadding(padded, bytes.length);\n\n    return padded;\n  }\n}\n"
  },
  {
    "path": "lib/network/util/file_read.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:path_provider/path_provider.dart';\n\nclass FileRead {\n  static String? userHome;\n\n  static Future<File> homeDir() async {\n    if (userHome != null) {\n      return File(\"${userHome!}${Platform.pathSeparator}.proxypin\");\n    }\n    if (Platforms.isDesktop()) {\n      userHome = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];\n    } else {\n      userHome = (await getApplicationSupportDirectory()).path;\n    }\n\n    var separator = Platform.pathSeparator;\n    return File(\"${userHome!}$separator.proxypin\");\n  }\n\n  static Future<String> readAsString(String file) async {\n    return rootBundle.loadString(file);\n    // return File(file).readAsString();\n  }\n\n  static Future<Uint8List> read(String file) async {\n    return rootBundle.load(file).then((bateData) => bateData.buffer.asUint8List());\n    // return File(file).readAsBytes();\n  }\n\n  static String? _uuid;\n\n  static Future<String> get iosUuid async {\n    if (_uuid == null) {\n      var applicationPath = (await getApplicationSupportDirectory()).path;\n      var uuidPattern = RegExp(r'/Application/([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})/');\n      var match = uuidPattern.firstMatch(applicationPath);\n\n      _uuid = match?.group(1);\n    }\n    return _uuid!;\n  }\n\n  static Future<Uint8List> readFile(String path) async {\n    if (Platform.isIOS) {\n      var uuid = await iosUuid;\n      //ios替换uuid\n      var uuidPattern = RegExp(r'/Application/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/');\n      path = path.replaceAll(uuidPattern, '/Application/$uuid/');\n    }\n\n    return File(path).readAsBytes();\n  }\n}\n"
  },
  {
    "path": "lib/network/util/lang.dart",
    "content": "import 'dart:typed_data';\n\ndynamic getFirstElement(List? list) {\n  return list?.firstOrNull;\n}\n\n///获取list元素类型\n/// @author wanghongen\nclass Lists {\n  static bool isNotEmpty(List? list) {\n    return list != null && list.isNotEmpty;\n  }\n\n  static Type getElementType(dynamic list) {\n    if (list == null || list.isEmpty || list is! List) {\n      return Null;\n    }\n\n    var type = list.first.runtimeType;\n\n    return type;\n  }\n\n  ///转换指定类型\n  static List<T> convertList<T>(List list) {\n    return list.map((e) => e as T).toList();\n  }\n}\n\nclass Strings {\n  ///\n  /// Splits the given String [s] in chunks with the given [chunkSize].\n  ///\n  static List<String> chunk(String s, int chunkSize) {\n    var chunked = <String>[];\n    for (var i = 0; i < s.length; i += chunkSize) {\n      var end = (i + chunkSize < s.length) ? i + chunkSize : s.length;\n      chunked.add(s.substring(i, end));\n    }\n    return chunked;\n  }\n\n  static bool isNotEmpty(String? s) {\n    return s != null && s.isNotEmpty;\n  }\n}\n\nclass HexUtils {\n  static String bytesToHex(List<int> bytes) {\n    return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();\n  }\n\n  static Uint8List decode(String hex) {\n    var str = hex.replaceAll(\" \", \"\");\n    str = str.toLowerCase();\n    if (str.length % 2 != 0) {\n      str = \"0$str\";\n    }\n    var l = str.length ~/ 2;\n    var result = Uint8List(l);\n    for (var i = 0; i < l; ++i) {\n      var x = int.parse(str.substring(i * 2, (2 * (i + 1))), radix: 16);\n      if (x.isNaN) {\n        throw ArgumentError('Expected hex string');\n      }\n      result[i] = x;\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "lib/network/util/localizations.dart",
    "content": "import 'dart:ui';\n\nimport 'package:proxypin/ui/configuration.dart';\n\n/// @author wanghongen\nclass Localizations {\n  static bool get isZH {\n    if (AppConfiguration.current?.language != null) {\n      return AppConfiguration.current?.language!.languageCode == 'zh';\n    }\n\n    return PlatformDispatcher.instance.locale.languageCode == 'zh';\n  }\n}\n"
  },
  {
    "path": "lib/network/util/logger.dart",
    "content": "import 'package:logger/logger.dart';\n\nfinal logger = Logger(\n    printer: PrettyPrinter(\n      methodCount: 0,\n      errorMethodCount: 15,\n      lineLength: 120,\n      colors: true,\n      printEmojis: false,\n      excludeBox: {Level.info: true, Level.debug: true},\n    ));\n"
  },
  {
    "path": "lib/network/util/process_info.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/native/installed_apps.dart';\nimport 'package:proxypin/native/process_info.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/util/socket_address.dart';\nimport 'package:win32audio/win32audio.dart';\n\nimport 'cache.dart';\n\nvoid main() async {\n  var processInfo = await ProcessInfoUtils.getProcess(512);\n  // await ProcessInfoUtils.getMacIcon(processInfo!.path);\n  // print(await ProcessInfoUtils.getProcessByPort(63194));\n  print(processInfo);\n}\n\n/// 进程信息工具类 用于获取进程信息\n///@author wanghongen\nclass ProcessInfoUtils {\n  static final processInfoCache = ExpiringCache<String, ProcessInfo>(const Duration(minutes: 5));\n\n  static Future<ProcessInfo?> getProcessByPort(InetSocketAddress socketAddress, String cacheKeyPre) async {\n    try {\n      if (Platform.isAndroid) {\n        var app = await ProcessInfoPlugin.getProcessByPort(socketAddress.host, socketAddress.port);\n        if (app != null) {\n          return app;\n        }\n        if (socketAddress.host == '127.0.0.1') {\n          return ProcessInfo('com.network.proxy', \"ProxyPin\", '', os: Platform.operatingSystem);\n        }\n        return null;\n      }\n\n      var pid = await _getPid(socketAddress);\n      if (pid == null) return null;\n\n      String cacheKey = \"$cacheKeyPre:$pid\";\n      var processInfo = processInfoCache.get(cacheKey);\n      if (processInfo != null) return processInfo;\n\n      processInfo = await getProcess(pid);\n      processInfoCache.set(cacheKey, processInfo!);\n      return processInfo;\n    } catch (e) {\n      logger.e(\"getProcessByPort error: $e\");\n      return null;\n    }\n  }\n\n  // 获取进程 ID\n  static Future<int?> _getPid(InetSocketAddress socketAddress) async {\n    if (Platform.isWindows) {\n      var result = await Process.run('cmd', ['/c', 'netstat -ano | findstr :${socketAddress.port}']);\n      var lines = LineSplitter.split(result.stdout);\n      for (var line in lines) {\n        var parts = line.trim().split(RegExp(r'\\s+'));\n        if (parts.length < 5) {\n          continue;\n        }\n        if (parts[1].trim().contains(\"${socketAddress.host}:${socketAddress.port}\")) {\n          return int.tryParse(parts[4]);\n        }\n      }\n      return null;\n    }\n\n    if (Platform.isMacOS) {\n      var results =\n          await Process.run('bash', ['-c', 'lsof -nP -iTCP:${socketAddress.port} |grep \"${socketAddress.port}->\"']);\n\n      if (results.exitCode != 0) {\n        return null;\n      }\n\n      var lines = LineSplitter.split(results.stdout);\n\n      for (var line in lines) {\n        var parts = line.trim().split(RegExp(r'\\s+'));\n        if (parts.length >= 9) {\n          return int.tryParse(parts[1]);\n        }\n      }\n    }\n    return null;\n  }\n\n  static Future<ProcessInfo?> getProcess(int pid) async {\n    if (Platform.isWindows) {\n      // 获取应用路径\n      var result = await Process.run('cmd', ['/c', 'wmic process where processid=$pid get ExecutablePath']);\n      var output = result.stdout.toString();\n      var path = output.split('\\n')[1].trim();\n      String name = path.substring(path.lastIndexOf('\\\\') + 1);\n      return ProcessInfo(name, name.split(\".\")[0], path, os: Platform.operatingSystem);\n    }\n\n    if (Platform.isMacOS) {\n      var results = await Process.run('bash', ['-c', 'ps -p $pid -o pid= -o comm=']);\n      if (results.exitCode == 0) {\n        var lines = LineSplitter.split(results.stdout);\n        for (var line in lines) {\n          var parts = line.trim().split(RegExp(r'\\s+'));\n          if (parts.length >= 2) {\n            parts.removeAt(0).trim();\n            var path = parts.join(\" \").split(\".app/\")[0];\n            String name = path.substring(path.lastIndexOf('/') + 1);\n            return ProcessInfo(name, name, \"$path.app\", os: Platform.operatingSystem);\n          }\n        }\n      }\n    }\n\n    return null;\n  }\n}\n\nclass ProcessInfo {\n  static final _iconCache = ExpiringCache<String, Uint8List?>(const Duration(minutes: 5));\n\n  final String id; //应用包名\n  final String name; //应用名称\n  final String path;\n  final String? os;\n\n  Uint8List? icon;\n  String? remoteHost;\n  int? remotePost;\n\n  ProcessInfo(this.id, this.name, this.path, {required this.os, this.icon, this.remoteHost, this.remotePost});\n\n  factory ProcessInfo.fromJson(Map<String, dynamic> json) {\n    return ProcessInfo(json['id'], json['name'], json['path'], os: json['os']);\n  }\n\n  bool get hasCacheIcon => icon != null || _iconCache.get(id) != null;\n\n  Uint8List? get cacheIcon => icon ?? _iconCache.get(id);\n\n  Future<Uint8List> getIcon() async {\n    if (icon != null) return icon!;\n    if (_iconCache.get(id) != null) return _iconCache.get(id)!;\n    try {\n      if (Platform.isAndroid) {\n        icon = (await InstalledApps.getAppInfo(id)).icon;\n      }\n\n      if ('windows' == os || path.endsWith('.exe')) {\n        icon = await _getWindowsIcon(path);\n      }\n\n      if (Platform.isMacOS) {\n        var macIcon = await _getMacIcon(path);\n        icon = await File(macIcon).readAsBytes();\n      }\n\n      icon = icon ?? Uint8List(0);\n      _iconCache.set(id, icon);\n    } catch (e) {\n      icon = Uint8List(0);\n    }\n    return icon!;\n  }\n\n  Future<Uint8List?> _getWindowsIcon(String path) async {\n    return await WinIcons().extractFileIcon(path);\n  }\n\n  static Future<String> _getMacIcon(String path) async {\n    var xml = await File('$path/Contents/Info.plist').readAsString();\n    var key = \"<key>CFBundleIconFile</key>\";\n    var indexOf = xml.indexOf(key);\n    var iconName = xml.substring(indexOf + key.length, xml.indexOf(\"</string>\", indexOf));\n    iconName = iconName.trim().replaceAll(\"<string>\", \"\");\n    var icon = iconName.endsWith(\".icns\") ? iconName : \"$iconName.icns\";\n    String iconPath = \"$path/Contents/Resources/$icon\";\n    return iconPath;\n  }\n\n  Map<String, dynamic> toJson() {\n    return {'id': id, 'name': name, 'path': path, 'os': os};\n  }\n\n  @override\n  String toString() {\n    return toJson().toString();\n  }\n}\n"
  },
  {
    "path": "lib/network/util/proxy_helper.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:proxypin/network/bin/listener.dart';\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/components/manager/script_manager.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/codec.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\nimport 'package:proxypin/network/util/crts.dart';\nimport 'package:proxypin/network/util/localizations.dart';\n\nimport '../components/host_filter.dart';\n\nclass ProxyHelper {\n  //请求本服务\n  static localRequest(ChannelContext channelContext, HttpRequest msg, Channel channel) async {\n    //获取配置\n    if (msg.path == '/config') {\n      final requestRewrites = await RequestRewriteManager.instance;\n      var response = HttpResponse(HttpStatus.ok, protocolVersion: msg.protocolVersion);\n      var body = {\n        \"requestRewrites\": await requestRewrites.toFullJson(),\n        'whitelist': HostFilter.whitelist.toJson(),\n        'blacklist': HostFilter.blacklist.toJson(),\n        'scripts': await ScriptManager.instance.then((script) {\n          var list = script.list.map((e) async {\n            return {'name': e.name, 'enabled': e.enabled, 'url': e.urls, 'script': await script.getScript(e)};\n          });\n          return Future.wait(list);\n        }),\n\n      };\n      response.body = utf8.encode(json.encode(body));\n      channel.writeAndClose(channelContext, response);\n      return;\n    }\n\n    var response = HttpResponse(HttpStatus.ok, protocolVersion: msg.protocolVersion);\n    response.body = utf8.encode('pong');\n    response.headers.set(\"os\", Platform.operatingSystem);\n    response.headers.set(\"hostname\", Platform.isAndroid ? Platform.operatingSystem : Platform.localHostname);\n    channel.writeAndClose(channelContext, response);\n  }\n\n  /// 下载证书\n  static void crtDownload(ChannelContext channelContext, Channel channel, HttpRequest request) async {\n    const String fileMimeType = 'application/x-x509-ca-cert';\n    var response = HttpResponse(HttpStatus.ok);\n    response.headers.set(HttpHeaders.CONTENT_TYPE, fileMimeType);\n    response.headers.set(\"Content-Disposition\", 'inline;filename=ProxyPinCA.crt');\n    response.headers.set(\"Connection\", 'close');\n\n    var caFile = await CertificateManager.certificateFile();\n    var caBytes = await caFile.readAsBytes();\n    response.headers.set(\"Content-Length\", caBytes.lengthInBytes.toString());\n\n    if (request.method == HttpMethod.head) {\n      channel.writeAndClose(channelContext, response);\n      return;\n    }\n    response.body = caBytes;\n    channel.writeAndClose(channelContext, response);\n  }\n\n  ///异常处理\n  static Future<void> exceptionHandler(\n      ChannelContext channelContext, Channel channel, EventListener? listener, HttpRequest? request, error) async {\n    HostAndPort? hostAndPort = channelContext.host;\n    hostAndPort ??= HostAndPort.host(\n        scheme: HostAndPort.httpScheme, channel.remoteSocketAddress.host, channel.remoteSocketAddress.port);\n    String message = error.toString();\n    HttpStatus status = HttpStatus(-1, message);\n    if (error is HandshakeException) {\n      status = HttpStatus(\n          -2,\n          Localizations.isZH\n              ? 'SSL handshake failed, 请检查证书安装是否正确'\n              : 'SSL handshake failed, please check the certificate');\n    } else if (error is ParserException) {\n      status = HttpStatus(-3, error.message);\n    } else if (error is SocketException) {\n      status = HttpStatus(-4, error.message);\n    } else if (error is SignalException) {\n      status.reason(Localizations.isZH ? '执行脚本异常' : 'Execute script exception');\n    }\n\n    request ??= HttpRequest(HttpMethod.connect, hostAndPort.domain)\n      ..body = message.codeUnits\n      ..headers.contentLength = message.codeUnits.length\n      ..hostAndPort = hostAndPort;\n    request.processInfo ??= channelContext.processInfo;\n\n    if (request.method == HttpMethod.connect && !request.uri.startsWith(\"http\")) {\n      request.uri = hostAndPort.domain;\n    }\n\n    if (request.response == null || request.method == HttpMethod.connect) {\n      request.response = HttpResponse(status)\n        ..headers.contentType = 'text/plain'\n        ..headers.contentLength = message.codeUnits.length\n        ..body = message.codeUnits;\n    }\n\n    request.response?.request = request;\n\n    channelContext.host = hostAndPort;\n\n    listener?.onRequest(channel, request);\n    listener?.onResponse(channelContext, request.response!);\n  }\n}\n"
  },
  {
    "path": "lib/network/util/random.dart",
    "content": "import 'dart:math';\n\nclass RandomUtil {\n  static const _characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';\n\n  static String randomString(int length) {\n    Random random = Random();\n    return String.fromCharCodes(Iterable.generate(\n      length,\n      (_) => _characters.codeUnitAt(random.nextInt(_characters.length)),\n    ));\n  }\n}\n"
  },
  {
    "path": "lib/network/util/socket_address.dart",
    "content": "import 'dart:io';\n\nclass InetSocketAddress {\n  final InternetAddress address;\n  final int port;\n\n  InetSocketAddress(this.address, this.port);\n\n  String get host => address.host;\n\n  @override\n  String toString() {\n    return \"InetSocketAddress($address:$port)\";\n  }\n}\n"
  },
  {
    "path": "lib/network/util/system_proxy.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:io';\n\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/utils/ip.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxy_manager/proxy_manager.dart';\n\n/// @author wanghongen\n/// 2023/7/26\nclass SystemProxy {\n  static SystemProxy? _instance;\n\n  ///单例\n  static SystemProxy get instance {\n    if (_instance == null) {\n      if (Platform.isMacOS) {\n        _instance = MacSystemProxy();\n      } else if (Platform.isWindows) {\n        _instance = WindowsSystemProxy();\n      } else if (Platform.isLinux) {\n        _instance = LinuxSystemProxy();\n      } else {\n        _instance = SystemProxy();\n      }\n    }\n    return _instance!;\n  }\n\n  ///获取代理忽略地址\n  static String get proxyPassDomains {\n    if (Platform.isMacOS) {\n      return '192.168.0.0/16;10.0.0.0/8;172.16.0.0/12;127.0.0.1;localhost;*.local;timestamp.apple.com';\n    }\n    if (Platform.isWindows) {\n      return '192.168.0.*;10.0.0.*;172.16.0.*;127.0.0.1;localhost;*.local;<local>';\n    }\n\n    if (Platform.isAndroid) {\n      return '192.168.0.0/16;10.0.0.0/8;172.16.0.0/12;127.0.0.1;localhost';\n    }\n    if (Platform.isIOS) {\n      return '192.168.0.0/16;10.0.0.0/8;172.16.0.0/12;127.0.0.1;localhost;*.local;timestamp.apple.com';\n    }\n\n    return '';\n  }\n\n  ///获取系统代理\n  static Future<ProxyInfo?> getSystemProxy(ProxyTypes types) async {\n    return instance._getSystemProxy(types);\n  }\n\n  ///设置系统代理\n  static Future<void> setSystemProxy(int port, bool sslSetting, String proxyPassDomains) async {\n    await instance._setSystemProxy(port, sslSetting, proxyPassDomains);\n  }\n\n  ///设置Https代理启用状态\n  static void setSslProxyEnable(bool proxyEnable, port) {\n    instance._setSslProxyEnable(proxyEnable, port);\n  }\n\n  /// 设置系统代理\n  /// @param sslSetting 是否设置https代理只在mac中有效\n  static Future<void> setSystemProxyEnable(int port, bool enable, bool sslSetting,\n      {required String passDomains}) async {\n    //启用系统代理\n    if (enable) {\n      await setSystemProxy(port, sslSetting, passDomains);\n      return;\n    }\n\n    await instance._setProxyEnable(enable, sslSetting);\n  }\n\n  ///设置代理忽略地址\n  static Future<void> setProxyPassDomains(String proxyPassDomains) async {\n    instance._setProxyPassDomains(proxyPassDomains);\n  }\n\n  //子类抽象方法\n\n  ///获取系统代理\n  Future<ProxyInfo?> _getSystemProxy(ProxyTypes types) async {\n    return null;\n  }\n\n  ///设置系统代理\n  Future<void> _setSystemProxy(int port, bool sslSetting, String proxyPassDomains) async {\n    ProxyManager manager = ProxyManager();\n    await manager.setAsSystemProxy(sslSetting ? ProxyTypes.https : ProxyTypes.http, \"127.0.0.1\", port);\n    setProxyPassDomains(proxyPassDomains);\n  }\n\n  ///设置代理是否启用\n  Future<void> _setProxyEnable(bool proxyEnable, bool sslSetting) async {\n    ProxyManager manager = ProxyManager();\n    await manager.cleanSystemProxy();\n  }\n\n  ///设置Https代理启用状态\n  Future<bool> _setSslProxyEnable(bool proxyEnable, int port) async {\n    return false;\n  }\n\n  ///设置代理忽略地址\n  Future<void> _setProxyPassDomains(String proxyPassDomains) async {}\n}\n\nclass MacSystemProxy implements SystemProxy {\n  static String? _hardwarePort;\n\n  // Helper to safely quote a string for sh (single-quote and escape any internal single quotes)\n  static String _shellQuote(String s) {\n    // Replace ' with '\\'' which is the safe way to include single quotes inside single-quoted strings in shell\n    return \"'${s.replaceAll(\"'\", \"'\\\\''\")}'\";\n  }\n\n  ///获取系统代理\n  @override\n  Future<ProxyInfo?> _getSystemProxy(ProxyTypes proxyTypes) async {\n    _hardwarePort = _hardwarePort ?? await hardwarePort();\n\n    // ensure we have a name\n    if (_hardwarePort == null || _hardwarePort!.isEmpty) {\n      logger.e('hardwarePort is empty, cannot get system proxy');\n      return null;\n    }\n\n    final quotedName = _shellQuote(_hardwarePort!);\n\n    var result = await Process.run('bash', [\n      '-c',\n      'networksetup ${proxyTypes == ProxyTypes.http ? '-getwebproxy' : '-getsecurewebproxy'} $quotedName'\n    ]).then((results) => results.stdout.toString().split('\\n'));\n\n    // defensive parsing: find lines safely\n    String enabledLine = result.firstWhere((item) => item.contains('Enabled'), orElse: () => '');\n    if (enabledLine.isEmpty) {\n      logger.e('Failed to parse Enabled line from networksetup output: ${result.join('\\n')}');\n      return null;\n    }\n\n    var proxyEnableParts = enabledLine.trim().split(RegExp(r\":\\s*\"));\n    var proxyEnable = proxyEnableParts.length > 1 ? proxyEnableParts[1] : 'No';\n    if (proxyEnable == 'No') {\n      return null;\n    }\n\n    String serverLine = result.firstWhere((item) => item.contains('Server'), orElse: () => '');\n    String portLine = result.firstWhere((item) => item.contains('Port'), orElse: () => '');\n    if (serverLine.isEmpty || portLine.isEmpty) {\n      logger.e('Failed to parse Server/Port from networksetup output: ${result.join('\\n')}');\n      return null;\n    }\n\n    var proxyServer = serverLine.trim().split(RegExp(r\":\\s*\"))[1];\n    var proxyPort = portLine.trim().split(RegExp(r\":\\s*\"))[1];\n    if (proxyEnable == 'Yes' && proxyServer.isNotEmpty) {\n      return ProxyInfo.of(proxyServer, int.parse(proxyPort));\n    }\n    return null;\n  }\n\n  ///mac设置代理地址\n  @override\n  Future<bool> _setSystemProxy(int port, bool sslSetting, String proxyPassDomains) async {\n    _hardwarePort = _hardwarePort ?? await hardwarePort();\n    if (_hardwarePort == null || _hardwarePort!.isEmpty) {\n      logger.e('hardwarePort is empty, cannot set system proxy');\n      return false;\n    }\n\n    final quotedName = _shellQuote(_hardwarePort!);\n\n    List<String> commands = [\n      'networksetup -setwebproxy $quotedName 127.0.0.1 $port',\n      sslSetting == true ? 'networksetup -setsecurewebproxy $quotedName 127.0.0.1 $port' : '',\n      'networksetup -setproxybypassdomains $quotedName ${proxyPassDomains.replaceAll(\";\", \" \")}',\n      'networksetup -setsocksfirewallproxystate $quotedName off',\n    ];\n    var results = await Process.run('bash', ['-c', _concatCommands(commands)]);\n    logger.d('set proxyServer, name: $_hardwarePort, exitCode: ${results.exitCode}, stdout: ${results.stdout}');\n    bool success = results.exitCode == 0;\n    if (!success) {\n      logger.e('setSystemProxy failed, stderr: ${results.stderr}');\n      return setProxyWithAuth(commands);\n    }\n    return success;\n  }\n\n  ///设置Https代理\n  @override\n  Future<bool> _setSslProxyEnable(bool proxyEnable, port) async {\n    var name = _hardwarePort ?? await hardwarePort();\n    if (name.isEmpty) {\n      logger.e('hardwarePort is empty, cannot set ssl proxy state');\n      return false;\n    }\n    final quotedName = _shellQuote(name);\n\n    List<String> commands = [\n      proxyEnable\n          ? 'networksetup -setsecurewebproxy $quotedName 127.0.0.1 $port'\n          : 'networksetup -setsecurewebproxystate $quotedName off'\n    ];\n\n    var results = await Process.run('bash', ['-c', _concatCommands(commands)]);\n    bool success = results.exitCode == 0;\n    if (!success) {\n      logger.e('setSystemProxy failed, stderr: ${results.stderr}');\n      return setProxyWithAuth(commands);\n    }\n    return success;\n  }\n\n  ///mac获取当前网络名称\n  static Future<String> hardwarePort() async {\n    var name = await networkName();\n    // Use a safer pipeline that avoids embedding awk's $2 (which complicates Dart string quoting).\n    // This command finds the Device line, takes the following Hardware Port line, and extracts the part after ':'\n    var cmd = 'networksetup -listnetworkserviceorder | grep \"Device: ${name}\" -A 1 | grep \"Hardware Port\" | cut -d: -f2 | sed -n \\'1p\\'';\n    var results = await Process.run('bash', ['-c', cmd]);\n    var out = results.stdout.toString().trim();\n    if (out.isEmpty) return '';\n    // split on newlines or commas and take the first non-empty token\n    var parts = out.split(RegExp(r\"[\\r\\n,]+\"));\n    return parts.first.trim();\n  }\n\n  ///设置代理忽略地址\n  @override\n  Future<void> _setProxyPassDomains(String proxyPassDomains) async {\n    _hardwarePort ??= await hardwarePort();\n    if (_hardwarePort == null || _hardwarePort!.isEmpty) {\n      logger.e('hardwarePort is empty, cannot set proxy bypass domains');\n      return;\n    }\n    final quotedName = _shellQuote(_hardwarePort!);\n    var results = await Process.run(\n        'bash', ['-c', 'networksetup -setproxybypassdomains $quotedName ${proxyPassDomains.replaceAll(\";\", \" \")}']);\n    logger.d('set proxyPassDomains, name: $_hardwarePort, exitCode: ${results.exitCode}, stdout: ${results.stdout}');\n  }\n\n  ///mac设置代理是否启用\n  @override\n  Future<void> _setProxyEnable(bool proxyEnable, bool sslSetting) async {\n    var proxyMode = proxyEnable ? 'on' : 'off';\n    _hardwarePort ??= await hardwarePort();\n    if (_hardwarePort == null || _hardwarePort!.isEmpty) {\n      logger.e('hardwarePort is empty, cannot set proxy enable state');\n      return;\n    }\n    logger.d('set proxyEnable: $proxyEnable, name: $_hardwarePort');\n    final quotedName = _shellQuote(_hardwarePort!);\n    List<String> commands = [\n      'networksetup -setwebproxystate $quotedName $proxyMode',\n      sslSetting ? 'networksetup -setsecurewebproxystate $quotedName $proxyMode' : ''\n    ];\n\n    var results = await Process.run('bash', ['-c', _concatCommands(commands)]);\n\n    if (results.exitCode != 0) {\n      logger.e('setProxyEnable failed, stderr: ${results.stderr}');\n      await setProxyWithAuth(commands);\n    }\n  }\n\n  Future<bool> setProxyWithAuth(List<String> commands) async {\n    // 使用 quoted form of 确保 shell 指令被 AppleScript 正确转义\n    String script = 'do shell script \"${commands.join('; ')}\" with administrator privileges';\n    try {\n      final result = await Process.run('osascript', ['-e', script]);\n      bool success = result.exitCode == 0;\n      if (!success) {\n        logger.e(\"操作失败或用户取消: ${result.stderr}\");\n      }\n      return success;\n    } catch (e) {\n      logger.e(\"执行 AppleScript 出错: $e\");\n      return false;\n    }\n  }\n\n  static String _concatCommands(List<String> commands) {\n    return commands.where((element) => element.isNotEmpty).join(' && ');\n  }\n}\n\nclass WindowsSystemProxy extends SystemProxy {\n  ///设置windows代理是否启用\n  @override\n  Future<void> _setProxyEnable(bool proxyEnable, bool sslSetting) async {\n    await _internetSettings('add', ['ProxyEnable', '/t', 'REG_DWORD', '/f', '/d', proxyEnable ? '1' : '0']);\n  }\n\n  ///获取系统代理\n  @override\n  Future<ProxyInfo?> _getSystemProxy(ProxyTypes types) async {\n    var results = await _internetSettings('query', ['ProxyEnable']);\n\n    var proxyEnableLine = results.split('\\r\\n').where((item) => item.contains('ProxyEnable')).first.trim();\n    if (proxyEnableLine.substring(proxyEnableLine.length - 1) != '1') {\n      return null;\n    }\n\n    return _internetSettings('query', ['ProxyServer']).then((results) {\n      var proxyServerLine = results.split('\\r\\n').where((item) => item.contains('ProxyServer')).firstOrNull;\n      var proxyServerLineSplits = proxyServerLine?.split(RegExp(r\"\\s+\"));\n\n      if (proxyServerLineSplits == null || proxyServerLineSplits.length < 2) {\n        return null;\n      }\n\n      var proxyLine = proxyServerLineSplits[proxyServerLineSplits.length - 1];\n      if (proxyLine.startsWith(\"http://\") || proxyLine.startsWith(\"https:///\")) {\n        proxyLine = proxyLine.replaceFirst(\"http://\", \"\").replaceFirst(\"https:///\", \"\");\n      }\n\n      var proxyServer = proxyLine.split(\":\")[0];\n      var proxyPort = proxyLine.split(\":\")[1];\n      logger.d(\"$proxyServer:$proxyPort\");\n      return ProxyInfo.of(proxyServer, int.parse(proxyPort));\n    }).catchError((e) {\n      logger.e('getSystemProxy error', error: e, stackTrace: StackTrace.current);\n      return null;\n    });\n  }\n\n  ///设置代理忽略地址\n  @override\n  Future<void> _setProxyPassDomains(String proxyPassDomains) async {\n    var results = await _internetSettings('add', ['ProxyOverride', '/t', 'REG_SZ', '/d', proxyPassDomains, '/f']);\n    logger.i('set proxyPassDomains, stdout: $results');\n  }\n\n  static Future<String> _internetSettings(String cmd, List<String> args) async {\n    return Process.run('reg', [\n      cmd,\n      'HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings',\n      '/v',\n      ...args,\n    ]).then((results) => results.stdout.toString());\n  }\n}\n\nclass LinuxSystemProxy extends SystemProxy {\n  @override\n  Future<void> _setSystemProxy(int port, bool sslSetting, String proxyPassDomains) async {\n    ProxyManager manager = ProxyManager();\n\n    await manager.setAsSystemProxy(ProxyTypes.http, \"127.0.0.1\", port);\n    if (sslSetting) await manager.setAsSystemProxy(ProxyTypes.https, \"127.0.0.1\", port);\n\n    SystemProxy.setProxyPassDomains(proxyPassDomains);\n  }\n\n  ///linux 获取代理\n  @override\n  Future<ProxyInfo?> _getSystemProxy(ProxyTypes types) async {\n    var mode = await Process.run(\"gsettings\", [\"get\", \"org.gnome.system.proxy\", \"mode\"])\n        .then((value) => value.stdout.toString().trim());\n    if (mode.contains(\"manual\")) {\n      var hostFuture = Process.run(\"gsettings\", [\"get\", \"org.gnome.system.proxy.${types.name}\", \"host\"])\n          .then((value) => value.stdout.toString().trim());\n      var portFuture = Process.run(\"gsettings\", [\"get\", \"org.gnome.system.proxy.${types.name}\", \"port\"])\n          .then((value) => value.stdout.toString().trim());\n\n      return Future.wait([hostFuture, portFuture]).then((value) {\n        var host = Strings.trimWrap(value[0], \"'\");\n        var port = Strings.trimWrap(value[1], \"'\");\n        if (host.isNotEmpty && port.isNotEmpty) {\n          return ProxyInfo.of(host, int.parse(port));\n        }\n        return null;\n      });\n    }\n    return null;\n  }\n}\n\nvoid main() async {\n  // single instance\n  ProxyManager manager = ProxyManager();\n// set a http proxy\n  await manager.setAsSystemProxy(ProxyTypes.http, \"127.0.0.1\", 1087);\n}\n"
  },
  {
    "path": "lib/network/util/task_queue.dart",
    "content": "import 'dart:async';\nimport 'dart:collection';\n\nclass SequentialTaskQueue {\n  final Queue<_Task> _tasks = Queue();\n  bool _isProcessing = false;\n  bool _isCancelled = false;\n  Completer<void>? _completer;\n\n  final Set<int> completedTasks = {};\n\n  final Map<int, List<_Task>> dependencyTasks = {};\n\n  /// Adds a task to the queue with a priority (e.g., streamId).\n  void add(int id, int? dependency, Future Function() task,\n      {void Function(dynamic error, StackTrace stackTrace)? onError}) {\n    if (_isCancelled) return;\n\n    _tasks.addLast(_Task(id, task, dependency: dependency, onError: onError));\n\n    // Sort tasks by priority (e.g., streamId).\n    // _tasks.sort((a, b) => a.key.compareTo(b.key));\n\n    runAllTask();\n  }\n\n  runAllTask() async {\n    if (!_isProcessing) {\n      _isProcessing = true;\n      _completer ??= Completer<void>();\n      while (_tasks.isNotEmpty) {\n        final currentTask = _tasks.removeFirst();\n        await runTask(currentTask);\n      }\n      _isProcessing = false;\n      _completer?.complete();\n      _completer = null;\n    }\n  }\n\n  Future<void> runTask(_Task task) async {\n    if (_isCancelled) return;\n\n    if (task.dependency != null && task.dependency! > 0 && !completedTasks.contains(task.dependency)) {\n      dependencyTasks[task.dependency!] ??= [];\n      dependencyTasks[task.dependency]!.add(task);\n    } else {\n      try {\n        await task.task();\n      } catch (error, stackTrace) {\n        task.onError?.call(error, stackTrace);\n      } finally {\n        completedTasks.add(task.id);\n      }\n\n      if (dependencyTasks[task.id] != null) {\n        for (var dependencyTask in dependencyTasks[task.id]!) {\n          await runTask(dependencyTask);\n        }\n        dependencyTasks.remove(task.id);\n      }\n    }\n  }\n\n  Future<void> waitForAll() async {\n    if (_isProcessing) {\n      _completer ??= Completer<void>();\n      return _completer?.future;\n    }\n    return;\n  }\n\n  void cancel() {\n    _isCancelled = true;\n    _tasks.clear();\n  }\n\n  void reset() {\n    _isCancelled = false;\n    _tasks.clear();\n  }\n}\n\nclass _Task {\n  final int id;\n  final int? dependency;\n  final Future Function() task;\n  final Function(dynamic error, StackTrace stackTrace)? onError;\n\n  _Task(this.id, this.task, {this.dependency, this.onError});\n}\n"
  },
  {
    "path": "lib/network/util/tls.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:typed_data';\n\nclass TLS {\n  ///从TLS Client Hello 获取支持的协议\n  static List<String>? supportProtocols(Uint8List data) {\n    try {\n      int sessionLength = data[43];\n      int pos = 44 + sessionLength;\n      if (data.length < pos + 2) return null;\n\n      int cipherSuitesLength = data.buffer.asByteData().getUint16(pos);\n      pos += 2 + cipherSuitesLength;\n      if (data.length < pos + 1) return null;\n\n      int compressionMethodsLength = data[pos];\n      pos += 1 + compressionMethodsLength;\n      if (data.length < pos + 2) return null;\n\n      int extensionsLength = data.buffer.asByteData().getUint16(pos);\n      pos += 2;\n      if (data.length < pos + extensionsLength) return null;\n\n      List<String> protocols = [];\n\n      int end = pos + extensionsLength;\n      while (pos + 4 <= end) {\n        int extensionType = data.buffer.asByteData().getUint16(pos);\n        int extensionLength = data.buffer.asByteData().getUint16(pos + 2);\n        pos += 4;\n\n        if (extensionType == 16 /* ALPN */) {\n          if (pos + 2 > end) return protocols;\n          int alpnExtensionLength = data.buffer.asByteData().getUint16(pos);\n          pos += 2;\n          if (pos + alpnExtensionLength > end) return protocols;\n\n          int alpnEnd = pos + alpnExtensionLength;\n          while (pos + 1 <= alpnEnd) {\n            int protocolLength = data[pos];\n            pos += 1;\n            if (pos + protocolLength > alpnEnd) return protocols;\n\n            String protocol = String.fromCharCodes(data.sublist(pos, pos + protocolLength));\n            protocols.add(protocol);\n\n            pos += protocolLength;\n          }\n        } else {\n          pos += extensionLength;\n        }\n      }\n      return protocols;\n    } catch (_) {\n      // Ignore errors, just return empty list\n    }\n\n    return null;\n  }\n\n  ///判断是否是TLS Client Hello\n  static bool isTLSClientHello(Uint8List data) {\n    if (data.length < 43) return false;\n    if (data[0] != 0x16 /* handshake */) return false;\n    if (data[1] != 0x03 || data[2] < 0x00 || data[2] > 0x03) return false;\n    if (data[5] != 0x01 /* client_hello */) return false;\n    if (data[9] != 0x03 || data[10] < 0x00 || data[10] > 0x03) return false;\n    return true;\n  }\n\n  ///从TLS Client Hello 解析域名\n  static String? getDomain(Uint8List data) {\n    try {\n      int sessionLength = data[43];\n      int pos = 44 + sessionLength;\n      if (data.length < pos + 2) return null;\n\n      int cipherSuitesLength = data.buffer.asByteData().getUint16(pos);\n      pos += 2 + cipherSuitesLength;\n      if (data.length < pos + 1) return null;\n\n      int compressionMethodsLength = data[pos];\n      pos += 1 + compressionMethodsLength;\n      if (data.length < pos + 2) return null;\n\n      int extensionsLength = data.buffer.asByteData().getUint16(pos);\n      pos += 2;\n      if (data.length < pos + extensionsLength) return null;\n\n      int end = pos + extensionsLength;\n      while (pos + 4 <= end) {\n        int extensionType = data.buffer.asByteData().getUint16(pos);\n        int extensionLength = data.buffer.asByteData().getUint16(pos + 2);\n        pos += 4;\n\n        if (extensionType == 0 /* server_name */) {\n          if (pos + 5 > end) return null;\n          int serverNameListLength = data.buffer.asByteData().getUint16(pos);\n          pos += 2;\n          if (pos + serverNameListLength > end) return null;\n\n          int serverNameType = data[pos];\n          int serverNameLength = data.buffer.asByteData().getUint16(pos + 1);\n          pos += 3;\n          if (serverNameType != 0 /* host_name */) return null;\n          if (pos + serverNameLength > end) return null;\n\n          return String.fromCharCodes(data.sublist(pos, pos + serverNameLength));\n        } else {\n          pos += extensionLength;\n        }\n      }\n    } catch (_) {\n// Ignore errors, just return null\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "lib/network/util/uri.dart",
    "content": "﻿import 'dart:collection';\n\n/// Uri构建工具类\nclass UriBuild {\n  /// 构建Uri\n  static Uri build(String url, {Map<String, String>? params}) {\n    var uri = Uri.parse(url);\n    if (params == null) {\n      return uri;\n    }\n    var queries = HashMap<String, String>();\n    queries.addAll(uri.queryParameters);\n    queries.addAll(params);\n\n    return uri.replace(queryParameters: queries);\n  }\n}\n\nclass UriUtils {\n  //map转url参数\n  static String mapToQuery(Map? map) {\n    if (map == null) {\n      return '';\n    }\n    List<String> list = [];\n    map.forEach((key, value) {\n      list.add('$key=${Uri.encodeComponent(value.toString())}');\n    });\n    return list.join('&');\n  }\n}\n"
  },
  {
    "path": "lib/storage/favorites.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:collection';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/websocket.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/storage/path.dart';\nimport 'package:proxypin/utils/har.dart';\n\n/// 收藏存储\n/// @author WangHongEn\nclass FavoriteStorage {\n  static Queue<Favorite>? list;\n\n  // Keep only recent websocket/sse messages per favorite to control favorites.json size.\n  static const int maxWebSocketMessagesPerFavorite = 200;\n  static const int maxWebSocketPayloadBytesPerFavorite = 1 * 1024 * 1024; //  1 MB\n\n  static Function()? addNotifier;\n\n  /// 获取收藏列表\n  static Future<Queue<Favorite>> get favorites async {\n    if (list == null) {\n      list = ListQueue();\n      var file = await Paths.getPath(\"favorites.json\");\n      if (await file.exists()) {\n        var value = await file.readAsString();\n        if (value.isEmpty) {\n          return list!;\n        }\n        try {\n          var config = jsonDecode(value) as List<dynamic>;\n          for (var element in config) {\n            list?.add(Favorite.fromJson(element));\n          }\n        } catch (e, t) {\n          logger.e('收藏列表解析失败', error: e, stackTrace: t);\n        }\n      }\n    }\n    return list!;\n  }\n\n  /// 添加收藏\n  static Future<void> addFavorite(HttpRequest request) async {\n    var favorites = await FavoriteStorage.favorites;\n    if (favorites.any((element) => element.request.requestId == request.requestId)) {\n      return;\n    }\n\n    // Snapshot to avoid mutating the live request/response when trimming persisted messages.\n    final favorite = _snapshotFavorite(request);\n    trimFavoriteMessages(favorite);\n    favorites.addFirst(favorite);\n    flushConfig();\n    //通知\n    addNotifier?.call();\n  }\n\n  static Favorite _snapshotFavorite(HttpRequest request) {\n    final copiedRequest = request.copy();\n    final copiedResponse = request.response?.copy();\n    return Favorite(copiedRequest, response: copiedResponse);\n  }\n\n  static Future<void> removeFavorite(Favorite favorite) async {\n    var list = await favorites;\n    list.remove(favorite);\n    flushConfig();\n  }\n\n  //刷新配置\n  static Future<void> flushConfig() async {\n    var list = await favorites;\n    await Paths.getPath(\"favorites.json\").then((file) => file.writeAsString(toJson(list)));\n  }\n\n  static String toJson(Queue<Favorite> list) {\n    return jsonEncode(list.map((e) => e.toJson()).toList());\n  }\n\n  /// Export all favorites to a given file path\n  static Future<void> exportToFile(String path) async {\n    var current = await favorites;\n    var content = toJson(current);\n    await File(path).writeAsString(content, flush: true);\n  }\n\n  /// Export all favorites as HAR to a given file path\n  static Future<void> exportToHarFile(String path, {String title = 'Favorites'}) async {\n    var current = await favorites;\n    final requests = current.map((f) => f.request).toList(growable: false);\n    await Har.writeFile(requests, File(path), title: title);\n  }\n\n  /// Import favorites from a JSON or HAR file (merges with current list, de-duping by requestId)\n  static Future<void> importFromFile(String path) async {\n    final file = File(path);\n    if (!await file.exists()) {\n      throw Exception('File not found');\n    }\n\n    final lower = path.toLowerCase();\n    List<Favorite> imported;\n    if (lower.endsWith('.har')) {\n      // HAR import\n      final requests = await Har.readFile(file);\n      imported = requests.map((r) => Favorite(r)).toList(growable: false);\n    } else {\n      // JSON import (old format)\n      final content = await file.readAsString();\n      if (content.trim().isEmpty) {\n        return;\n      }\n      final decoded = jsonDecode(content) as List<dynamic>;\n      imported = decoded.map((e) => Favorite.fromJson(e as Map<String, dynamic>)).toList(growable: false);\n    }\n\n    final current = await favorites;\n    final existingIds = current.map((e) => e.request.requestId).toSet();\n\n    // Merge without replacing current entries; skip duplicates by requestId\n    for (var fav in imported.reversed) {\n      final rid = fav.request.requestId;\n      if (existingIds.contains(rid)) {\n        continue;\n      }\n      trimFavoriteMessages(fav);\n      existingIds.add(rid);\n      current.addFirst(fav);\n    }\n\n    await flushConfig();\n    addNotifier?.call();\n  }\n\n  static bool trimFavoriteMessages(Favorite favorite) {\n    final response = favorite.response;\n    final requestFrames = List.of(favorite.request.messages);\n    final responseFrames = List.of(response?.messages ?? const []);\n\n    if (requestFrames.isEmpty && responseFrames.isEmpty) {\n      return false;\n    }\n\n    final refs = <_FrameRef>[\n      ...requestFrames.map((e) => _FrameRef(isRequest: true, frame: e)),\n      ...responseFrames.map((e) => _FrameRef(isRequest: false, frame: e)),\n    ]..sort((a, b) => a.frame.time.compareTo(b.frame.time));\n\n    final totalBytes = refs.fold<int>(0, (sum, e) => sum + e.frame.payloadData.length);\n    if (refs.length <= maxWebSocketMessagesPerFavorite && totalBytes <= maxWebSocketPayloadBytesPerFavorite) {\n      return false;\n    }\n\n    final kept = <_FrameRef>[];\n    int keptBytes = 0;\n    for (int i = refs.length - 1; i >= 0; i--) {\n      final ref = refs[i];\n      final bytes = ref.frame.payloadData.length;\n      final hitCount = kept.length >= maxWebSocketMessagesPerFavorite;\n      final hitBytes = kept.isNotEmpty && (keptBytes + bytes > maxWebSocketPayloadBytesPerFavorite);\n      if (hitCount || hitBytes) {\n        continue;\n      }\n      kept.add(ref);\n      keptBytes += bytes;\n      if (kept.length >= maxWebSocketMessagesPerFavorite) {\n        break;\n      }\n    }\n\n    kept.sort((a, b) => a.frame.time.compareTo(b.frame.time));\n    favorite.request.messages = kept.where((e) => e.isRequest).map((e) => e.frame).toList(growable: false);\n    response?.messages = kept.where((e) => !e.isRequest).map((e) => e.frame).toList(growable: false);\n    return true;\n  }\n}\n\nclass _FrameRef {\n  final bool isRequest;\n  final WebSocketFrame frame;\n\n  _FrameRef({required this.isRequest, required this.frame});\n}\n\nclass Favorite {\n  String? name;\n  final HttpRequest request;\n  HttpResponse? response;\n\n  Favorite(this.request, {this.name, this.response}) {\n    response ??= request.response;\n    request.response = response;\n    response?.request = request;\n  }\n\n  factory Favorite.fromJson(Map<String, dynamic> json) {\n    return Favorite(HttpRequest.fromJson(json['request']),\n        name: json['name'], response: json['response'] == null ? null : HttpResponse.fromJson(json['response']));\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'name': name,\n      'request': request.toJson(),\n      'response': response?.toJson(),\n    };\n  }\n\n  int get websocketMessageCount => request.messages.length + (response?.messages.length ?? 0);\n}\n"
  },
  {
    "path": "lib/storage/histories.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:async';\nimport 'dart:collection';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:date_format/date_format.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/storage/path.dart';\nimport 'package:proxypin/utils/files.dart';\nimport 'package:proxypin/utils/har.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:share_plus/share_plus.dart';\n\n///历史存储\n///@Author WangHongEn\nclass HistoryStorage {\n  static HistoryStorage? _instance;\n  final File _storageFile;\n\n  HistoryStorage._internal(this._storageFile);\n\n  static final ListenableList<HistoryItem> _histories = ListenableList();\n\n  ///单例\n  static Future<HistoryStorage> get instance async {\n    if (_instance == null) {\n      var file = await Paths.getPath(\"histories.json\");\n      _instance = HistoryStorage._internal(file);\n      await _instance!._init();\n    }\n    return _instance!;\n  }\n\n  //初始化\n  Future<void> _init() async {\n    if (await _storageFile.exists()) {\n      var content = await _storageFile.readAsString();\n      if (content.trim().isEmpty) {\n        return;\n      }\n      try {\n        var list = jsonDecode(content) as List<dynamic>;\n        for (var entry in list) {\n          _histories.add(HistoryItem.formJson(entry));\n        }\n      } catch (e) {\n        logger.e(\"历史记录解析错误\", error: e);\n      }\n    }\n  }\n\n  static Future<String> _homePath() async {\n    final home = await getApplicationSupportDirectory();\n    return '${home.path}${Platform.pathSeparator}history';\n  }\n\n  /// 获取历史记录\n  List<HistoryItem> get histories {\n    return _histories.source;\n  }\n\n  addListener(ListenerListEvent<HistoryItem> listener) async {\n    _histories.addListener(listener);\n  }\n\n  ///打开文件\n  static Future<File> openFile(String name) async {\n    final homePath = await _homePath();\n    var file = File('$homePath${Platform.pathSeparator}$name');\n    return file.create(recursive: true);\n  }\n\n  /// 添加历史记录\n  HistoryItem addHistory(String name, File file, int requestLength) {\n    var historyItem = HistoryItem(name, file.path, requestLength, 0);\n    _histories.add(historyItem);\n    refresh();\n    return historyItem;\n  }\n\n  int getIndex(HistoryItem item) {\n    return _histories.indexOf(item);\n  }\n\n  //更新\n  Future<void> updateHistory(int index, HistoryItem item) async {\n    _histories.update(index, item);\n    refresh();\n  }\n\n  //获取\n  HistoryItem getHistory(int index) {\n    return _histories.source[index];\n  }\n\n  Future<void> refresh() async {\n    await _storageFile.writeAsString(jsonEncode(_histories.source));\n  }\n\n  ///删除\n  Future<void> removeHistory(int index) async {\n    var history = _histories.removeAt(index);\n    logger.i('删除历史记录 $history');\n    final homePath = await _homePath();\n    var file = File('$homePath${Platform.pathSeparator}${Files.getName(history.path)}');\n    file.delete();\n    await refresh();\n  }\n\n  //获取请求列表\n  Future<List<HttpRequest>> getRequests(HistoryItem history) async {\n    if (history.requests == null) {\n      final homePath = await _homePath();\n      String path = '$homePath${Platform.pathSeparator}${Files.getName(history.path)}';\n      var file = File(path);\n      history.requests = await Har.readFile(file);\n      history.requestLength = history.requests!.length;\n      file.length().then((size) => history.fileSize = size);\n    }\n\n    return history.requests!;\n  }\n\n  ///刷新requests\n  Future<void> flushRequests(HistoryItem history, List<HttpRequest> requests) async {\n    logger.i(\"刷新历史记录 $history\");\n    final homePath = await _homePath();\n    String path = '$homePath${Platform.pathSeparator}${Files.getName(history.path)}';\n    var file = File(path);\n    for (int i = 0; i < requests.length; i++) {\n      var request = requests[i];\n      var har = Har.toHar(request);\n      await file.writeAsString(\"${jsonEncode(har)},\\n\", mode: i == 0 ? FileMode.write : FileMode.append);\n    }\n\n    history.requestLength = requests.length;\n    await file.length().then((size) => history.fileSize = size);\n    await refresh();\n  }\n\n  //添加历史\n  Future<HistoryItem> addHarFile(XFile file) async {\n    var readAsBytes = await file.readAsString();\n    var json = jsonDecode(readAsBytes);\n    var log = json['log'];\n    String name = formatDate(DateTime.now(), [mm, '-', d, ' ', HH, ':', nn, ':', ss]);\n    List? pages = log['pages'] as List?;\n    if (pages?.isNotEmpty == true) {\n      name = pages?.first['title'];\n    }\n\n    //解析请求\n    List entries = log['entries'];\n    var list = entries.map((e) => Har.toRequest(e)).toList();\n\n    //保存文件\n    var historyFile = await HistoryStorage.openFile(\"${DateTime.now().millisecondsSinceEpoch}.txt\");\n    var open = await historyFile.open(mode: FileMode.append);\n    for (var request in list) {\n      await open.writeString(jsonEncode(Har.toHar(request)));\n      await open.writeString(\",\\n\");\n    }\n    return addHistory(name, historyFile, list.length);\n  }\n}\n\nclass HistoryTask extends ListenerListEvent<HttpRequest> {\n  HistoryItem? history;\n  Timer? timer;\n  final Queue writeList = Queue();\n\n  RandomAccessFile? open;\n  bool locked = false;\n\n  static HistoryTask? _instance;\n\n  final Configuration configuration;\n  final ListenableList<HttpRequest> sourceList;\n\n  HistoryTask(this.configuration, this.sourceList) {\n    logger.d(\"start history task\");\n    if (configuration.historyCacheTime != 0) {\n      sourceList.addListener(this);\n      Future.delayed(const Duration(seconds: 3), () => cleanHistory());\n    }\n  }\n\n  static HistoryTask ensureInstance(Configuration configuration, ListenableList<HttpRequest> sourceList) {\n    return _instance ??= HistoryTask(configuration, sourceList);\n  }\n\n  //清理历史数据\n  Future<void> cleanHistory() async {\n    if (configuration.historyCacheTime == 0) {\n      return;\n    }\n    var overdueTime = DateTime.now().subtract(Duration(days: configuration.historyCacheTime));\n    var historyStorage = await HistoryStorage.instance;\n    var histories = historyStorage.histories;\n    for (int i = 0; i < histories.length; i++) {\n      if (histories.elementAt(i).createTime.isBefore(overdueTime)) {\n        await historyStorage.removeHistory(i);\n        i--;\n      }\n    }\n  }\n\n  @override\n  void onAdd(HttpRequest item) {\n    if (history == null) {\n      startTask();\n      return;\n    }\n    writeList.add(item);\n  }\n\n  @override\n  void onRemove(HttpRequest item) => resetList();\n\n  @override\n  void onBatchRemove(List<HttpRequest> items) => resetList();\n\n  @override\n  clear() => resetList();\n\n  Future<void> resetList() async {\n    locked = true;\n    await open?.lock().timeout(Duration(seconds: 3), onTimeout: () => open!.unlock());\n    open = await open?.truncate(0);\n    await open?.setPosition(0);\n    history?.requestLength = 0;\n    history?.requests = null;\n    writeList.clear();\n    writeList.addAll(sourceList.source);\n    locked = false;\n    open?.unlock();\n  }\n\n  void cancelTask() {\n    timer?.cancel();\n    timer = null;\n    open?.close();\n    open = null;\n    history = null;\n    sourceList.removeListener(this);\n    writeList.clear();\n  }\n\n  //写入任务\n  Future<void> startTask() async {\n    if (history != null || locked) return;\n    locked = true;\n\n    HistoryStorage storage = await HistoryStorage.instance;\n    var name = formatDate(DateTime.now(), [mm, '-', d, ' ', HH, ':', nn, ':', ss]);\n    File file = await HistoryStorage.openFile(\"${DateTime.now().millisecondsSinceEpoch}.txt\");\n    history = storage.addHistory(name, file, 0);\n    writeList.clear();\n    writeList.addAll(sourceList.source);\n    locked = false;\n\n    open = await file.open(mode: FileMode.append);\n    timer = Timer.periodic(const Duration(seconds: 5), (it) => writeTask());\n  }\n\n  //写入任务\n  Future<void> writeTask() async {\n    if (writeList.isEmpty) {\n      return;\n    }\n\n    bool changed = false;\n    while (writeList.isNotEmpty && !locked) {\n      var request = writeList.removeFirst();\n      var har = Har.toHar(request);\n\n      await open?.writeString(\"${jsonEncode(har)},\\n\");\n\n      history!.requestLength++;\n      changed = true;\n    }\n\n    if (!changed) return;\n    history!.fileSize = await open!.length();\n    history!.requests = null;\n    var historyStorage = await HistoryStorage.instance;\n    historyStorage.updateHistory(historyStorage.getIndex(history!), history!);\n  }\n}\n\n/// 历史记录\nclass HistoryItem {\n  String name;\n  final String path; // 文件路径\n  int requestLength = 0; // 请求数量\n  int? fileSize; // 文件大小\n  DateTime createTime = DateTime.now();\n\n  List<HttpRequest>? requests;\n\n  HistoryItem(this.name, this.path, this.requestLength, this.fileSize, {DateTime? createTime})\n      : createTime = createTime ?? DateTime.now();\n\n  //json反序列化\n  factory HistoryItem.formJson(Map<String, dynamic> map) {\n    return HistoryItem(map['name'], map['path'], map['requestLength'], map['fileSize'],\n        createTime: map['createTime'] == null ? null : DateTime.fromMillisecondsSinceEpoch(map['createTime']));\n  }\n\n  //json序列化\n  Map<String, dynamic> toJson() {\n    return {\n      'name': name,\n      'path': path,\n      'requestLength': requestLength,\n      'fileSize': fileSize,\n      'createTime': createTime.millisecondsSinceEpoch,\n    };\n  }\n\n  //获取文件大小\n  String get size {\n    if (this.fileSize == null) {\n      return \"\";\n    }\n\n    int fileSize = this.fileSize!;\n    if (fileSize > 1024 * 1024) {\n      return \"${(fileSize / 1024 / 1024).toStringAsFixed(1)}MB\";\n    }\n\n    return \"${(fileSize / 1024).toStringAsFixed(1)}KB\";\n  }\n\n  @override\n  String toString() {\n    return \"$path $requestLength $fileSize\";\n  }\n}\n"
  },
  {
    "path": "lib/storage/local_storage.dart",
    "content": "import 'package:shared_preferences/shared_preferences.dart';\n\nclass LocalStorage {\n  static Future<bool?> getBool(String key, {bool? defaultValue}) async {\n    SharedPreferences prefs = await SharedPreferences.getInstance();\n    return prefs.getBool(key) ?? defaultValue;\n  }\n\n  static Future<void> setBool(String key, bool value) async {\n    SharedPreferences prefs = await SharedPreferences.getInstance();\n    await prefs.setBool(key, value);\n  }\n}\n"
  },
  {
    "path": "lib/storage/path.dart",
    "content": "import 'dart:io';\n\nimport 'package:path_provider/path_provider.dart';\n\nclass Paths {\n  static final Map<String, File> _cache = {};\n\n  //获取配置路径\n  static Future<File> getPath(String fileName) async {\n    if (_cache.containsKey(fileName)) {\n      return _cache[fileName]!;\n    }\n\n    final directory = await getApplicationSupportDirectory();\n    var file = File('${directory.path}${Platform.pathSeparator}$fileName');\n\n    if (!await file.exists()) {\n      await file.create(recursive: true);\n    }\n    _cache[fileName] = file;\n    return file;\n  }\n\n  static Future<File> createFile(String dir, String filename) async {\n    final directory = await getApplicationSupportDirectory();\n    var file = File('${directory.path}${Platform.pathSeparator}$dir${Platform.pathSeparator}$filename');\n    return file.create(recursive: true);\n  }\n}\n"
  },
  {
    "path": "lib/storage/shared_preference_keys.dart",
    "content": "// ignore_for_file: constant_identifier_names\n\nclass SharedPreferenceKeys {\n  static const String CERT_INSTALL_SKIP = \"cert_install_skip\";\n}\n"
  },
  {
    "path": "lib/ui/app_update/app_update_repository.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter/cupertino.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/app_update/remote_version_entity.dart';\nimport 'package:proxypin/ui/component/app_dialog.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'constants.dart';\nimport 'new_version_dialog.dart';\n\nclass AppUpdateRepository {\n  static final HttpClient httpClient = HttpClient();\n\n  static Future<void> checkUpdate(BuildContext context, {bool canIgnore = true, bool showToast = false}) async {\n    try {\n      var lastVersion = await getLatestVersion();\n      if (lastVersion == null) {\n        logger.w(\"[AppUpdate] failed to fetch latest version info\");\n        return;\n      }\n\n      if (!context.mounted) return;\n\n      var availableUpdates = compareVersions(AppConfiguration.version, lastVersion.version);\n      if (availableUpdates) {\n        if (canIgnore) {\n          var ignoreVersion = await SharedPreferencesAsync().getString(Constants.ignoreReleaseVersionKey);\n          if (ignoreVersion == lastVersion.version) {\n            logger.d(\"ignored release [${lastVersion.version}]\");\n            return;\n          }\n        }\n\n        logger.d(\"new version available: $lastVersion\");\n\n        if (!context.mounted) return;\n        NewVersionDialog(\n          AppConfiguration.version,\n          lastVersion,\n          canIgnore: true,\n        ).show(context);\n        return;\n      }\n\n      logger.i(\"already using latest version[${AppConfiguration.version}], last: [${lastVersion.version}]\");\n\n      if (showToast) {\n        AppLocalizations localizations = AppLocalizations.of(context)!;\n        CustomToast.success(localizations.appUpdateNotAvailableMsg).show(context);\n      }\n    } catch (e) {\n      logger.e(\"Error checking for updates: $e\");\n      if (showToast) {\n        CustomToast.error(e.toString()).show(context);\n      }\n    }\n  }\n\n  /// Fetches the latest version information from the GitHub releases API.\n  static Future<RemoteVersionEntity?> getLatestVersion({bool includePreReleases = false}) async {\n    final response = await http.get(Uri.parse(Constants.githubReleasesApiUrl));\n    if (response.statusCode != 200 || response.body.isEmpty) {\n      logger.w(\"[AppUpdate] failed to fetch latest version info\");\n      return null;\n    }\n\n    var body = jsonDecode(response.body) as List;\n    final releases = body.map((e) => GithubReleaseParser.parse(e as Map<String, dynamic>));\n    late RemoteVersionEntity latest;\n    if (includePreReleases) {\n      latest = releases.first;\n    } else {\n      latest = releases.firstWhere((e) => e.preRelease == false);\n    }\n\n    logger.d(\"[AppUpdate] latest version: $latest\");\n    return latest;\n  }\n\n  static bool compareVersions(String currentVersion, String latestVersion) {\n    String normalizeVersion(String version) {\n      return version.startsWith('v') ? version.substring(1) : version;\n    }\n\n    List<int> parseVersion(String version) {\n      return normalizeVersion(version).split('.').map(int.parse).toList();\n    }\n\n    List<int> current = parseVersion(currentVersion);\n    List<int> latest = parseVersion(latestVersion);\n\n    for (int i = 0; i < current.length; i++) {\n      if (i >= latest.length || current[i] > latest[i]) {\n        return false; // 当前版本高于最新版本\n      } else if (current[i] < latest[i]) {\n        return true; // 需要更新\n      }\n    }\n\n    return latest.length > current.length; // 最新版本有更多的子版本号\n  }\n}\n"
  },
  {
    "path": "lib/ui/app_update/constants.dart",
    "content": "abstract class Constants {\n  static const githubUrl = \"https://github.com/wanghongenpin/proxypin\";\n  static const githubReleasesApiUrl =\n      \"https://api.github.com/repos/wanghongenpin/proxypin/releases\";\n  static const githubLatestReleaseUrl =\n      \"https://github.com/wanghongenpin/proxypin/releases/latest\";\n\n  static const String ignoreReleaseVersionKey = \"ignored_release_version\";\n}\n\nconst kAnimationDuration = Duration(milliseconds: 250);\n"
  },
  {
    "path": "lib/ui/app_update/new_version_dialog.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/app_update/remote_version_entity.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nimport 'constants.dart';\n\nclass NewVersionDialog extends StatelessWidget {\n  NewVersionDialog(\n    this.currentVersion,\n    this.newVersion, {\n    this.canIgnore = true,\n  }) : super(key: _dialogKey);\n\n  final String currentVersion;\n  final RemoteVersionEntity newVersion;\n  final bool canIgnore;\n\n  static final _dialogKey = GlobalKey(debugLabel: 'new version dialog');\n\n  Future<void> show(BuildContext context) async {\n    if (_dialogKey.currentContext == null) {\n      return showDialog(\n        context: context,\n        useRootNavigator: true,\n        builder: (context) => this,\n      );\n    } else {\n      logger.d(\"new version dialog is already open\");\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    return AlertDialog(\n      title: Text(localizations.appUpdateDialogTitle),\n      // scrollable: true,\n      content: Container(\n          constraints: BoxConstraints(maxHeight: 230, maxWidth: 500),\n          child: SingleChildScrollView(\n              child: Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Text(localizations.appUpdateUpdateMsg),\n              const SizedBox(height: 5),\n              Text.rich(\n                TextSpan(\n                  children: [\n                    TextSpan(text: \"${localizations.appUpdateCurrentVersionLbl}: \", style: theme.textTheme.bodySmall),\n                    TextSpan(text: currentVersion, style: theme.textTheme.labelMedium),\n                  ],\n                ),\n              ),\n              Text.rich(\n                TextSpan(\n                  children: [\n                    TextSpan(text: \"${localizations.appUpdateNewVersionLbl}: \", style: theme.textTheme.bodySmall),\n                    TextSpan(text: newVersion.version, style: theme.textTheme.labelMedium),\n                  ],\n                ),\n              ),\n              Text(newVersion.content ?? '', style: theme.textTheme.labelMedium),\n            ],\n          ))),\n      actions: [\n        Wrap(alignment: WrapAlignment.end, children: [\n          if (canIgnore)\n            TextButton(\n              onPressed: () async {\n                SharedPreferencesAsync().setString(Constants.ignoreReleaseVersionKey, newVersion.version);\n                logger.i(\"ignored release [${newVersion.version}]\");\n                if (context.mounted) Navigator.pop(context);\n              },\n              child: Text(localizations.appUpdateIgnoreBtnTxt),\n            ),\n          TextButton(\n            onPressed: () => Navigator.pop(context),\n            child: Text(localizations.appUpdateLaterBtnTxt),\n          ),\n          TextButton(\n            onPressed: () async {\n              await launchUrl(Uri.parse(newVersion.url), mode: LaunchMode.externalApplication);\n            },\n            child: Text(localizations.appUpdateUpdateNowBtnTxt),\n          ),\n        ])\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/app_update/remote_version_entity.dart",
    "content": "import 'package:proxypin/utils/lang.dart';\n\nclass RemoteVersionEntity {\n  final String version;\n  final String buildNumber;\n  final String releaseTag;\n  final bool preRelease;\n  final String url;\n  final String? content;\n  final DateTime publishedAt;\n\n  RemoteVersionEntity({\n    required this.version,\n    required this.buildNumber,\n    required this.releaseTag,\n    required this.preRelease,\n    required this.url,\n    this.content,\n    required this.publishedAt,\n  });\n\n  @override\n  String toString() {\n    return 'RemoteVersionEntity(version: $version, buildNumber: $buildNumber, releaseTag: $releaseTag, preRelease: $preRelease, url: $url, publishedAt: $publishedAt)';\n  }\n}\n\nabstract class GithubReleaseParser {\n  static RemoteVersionEntity parse(Map<String, dynamic> json) {\n    final fullTag = json['tag_name'] as String;\n    final fullVersion = fullTag.removePrefix(\"v\").split(\"-\").first.split(\"+\");\n    var version = fullVersion.first;\n    var buildNumber = fullVersion.elementAtOrElse(1, (index) => \"\");\n\n    final preRelease = json[\"prerelease\"] as bool;\n    final publishedAt = DateTime.parse(json[\"published_at\"] as String);\n\n    var body = json['body']?.toString().split(\"English: \");\n    return RemoteVersionEntity(\n        version: version,\n        buildNumber: buildNumber,\n        releaseTag: fullTag,\n        preRelease: preRelease,\n        url: json[\"html_url\"] as String,\n        content: body?.last,\n        publishedAt: publishedAt);\n  }\n}\n\n"
  },
  {
    "path": "lib/ui/component/app_dialog.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:toastification/toastification.dart';\n\nclass AppAlertDialog extends StatelessWidget {\n  const AppAlertDialog({\n    super.key,\n    this.title,\n    required this.message,\n  });\n\n  final String? title;\n  final String message;\n\n  factory AppAlertDialog.fromErr(({String type, String? message}) err) => AppAlertDialog(\n        title: err.message == null ? null : err.type,\n        message: err.message ?? err.type,\n      );\n\n  Future<void> show(BuildContext context) async {\n    await showDialog(\n      context: context,\n      useRootNavigator: true,\n      builder: (context) => this,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final localizations = MaterialLocalizations.of(context);\n\n    return AlertDialog(\n      title: title != null ? Text(title!) : null,\n      content: SingleChildScrollView(\n        child: SizedBox(\n          width: 468,\n          child: Text(message),\n        ),\n      ),\n      actions: [\n        TextButton(\n          onPressed: () {\n            Navigator.of(context).pop();\n          },\n          child: Text(localizations.okButtonLabel),\n        ),\n      ],\n    );\n  }\n}\n\nenum AlertType {\n  info,\n  error,\n  success;\n\n  ToastificationType get _toastificationType => switch (this) {\n        success => ToastificationType.success,\n        error => ToastificationType.error,\n        info => ToastificationType.info,\n      };\n}\n\nclass CustomToast extends StatelessWidget {\n  const CustomToast(\n    this.message, {\n    super.key,\n    this.type = AlertType.info,\n    this.icon,\n    this.duration = const Duration(seconds: 3),\n  });\n\n  const CustomToast.error(\n    this.message, {\n    super.key,\n    this.duration = const Duration(seconds: 5),\n  })  : type = AlertType.error,\n        icon = Icons.error;\n\n  const CustomToast.success(\n    this.message, {\n    super.key,\n    this.duration = const Duration(seconds: 2),\n  })  : type = AlertType.success,\n        icon = Icons.check_circle;\n\n  final String message;\n  final AlertType type;\n  final IconData? icon;\n  final Duration duration;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      decoration: BoxDecoration(\n        borderRadius: const BorderRadius.all(Radius.circular(4)),\n        color: Theme.of(context).colorScheme.surface,\n      ),\n      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Flexible(child: Text(message)),\n        ],\n      ),\n    );\n  }\n\n  void show(BuildContext context, {Alignment alignment = Alignment.bottomLeft}) {\n    toastification.show(\n      context: context,\n      title: Text(message),\n      icon: icon == null ? null : Icon(icon),\n      type: type._toastificationType,\n      alignment: alignment,\n      autoCloseDuration: duration,\n      style: ToastificationStyle.flat,\n      pauseOnHover: true,\n      showProgressBar: false,\n      dragToClose: true,\n      closeOnClick: true,\n      closeButton: ToastCloseButton(showType: CloseButtonShowType.onHover),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/buttons.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass Buttons {\n  static ButtonStyle get buttonStyle => ButtonStyle(\n      padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),\n      shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n          RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))));\n}\n"
  },
  {
    "path": "lib/ui/component/chinese_font.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass SystemChineseFont {\n  const SystemChineseFont._();\n\n  /// Chinese font family fallback, for windows\n  static const List<String> windowsFontFamily = [\n    'Microsoft YaHei',\n  ];\n\n  static const systemFont = \"system-font\";\n\n  static bool systemFontLoaded = false;\n\n  /// Chinese font family fallback, for most platforms\n  static List<String> get fontFamilyFallback {\n    return [\n      systemFont,\n      \"sans-serif\",\n      ...windowsFontFamily,\n    ];\n  }\n\n  /// Text style with updated fontFamilyFallback & fontVariations\n  static TextStyle get textStyle {\n    return const TextStyle().useSystemChineseFont();\n  }\n\n  /// Text theme with updated fontFamilyFallback & fontVariations\n  static TextTheme get textTheme {\n    return Typography().dense.apply(fontFamilyFallback: fontFamilyFallback);\n  }\n}\n\nextension TextStyleUseSystemChineseFont on TextStyle {\n  /// Add fontFamilyFallback & fontVariation to original font style\n  TextStyle useSystemChineseFont() {\n    return copyWith(\n      fontFamilyFallback: [\n        ...?fontFamilyFallback,\n        ...SystemChineseFont.fontFamilyFallback,\n      ],\n      fontVariations: [\n        ...?fontVariations,\n        if (fontWeight != null) FontVariation('wght', (fontWeight!.index + 1) * 100),\n      ],\n    );\n  }\n}\n\nextension TextThemeUseSystemChineseFont on TextTheme {\n  /// Add fontFamilyFallback & fontVariation to original text theme\n  TextTheme useSystemChineseFont() {\n    return SystemChineseFont.textTheme.merge(this);\n  }\n}\n\nextension ThemeDataUseSystemChineseFont on ThemeData {\n  /// Add fontFamilyFallback & fontVariation to original theme data\n  ThemeData useSystemChineseFont() {\n    return copyWith(textTheme: textTheme.useSystemChineseFont());\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/context_menu_region.dart",
    "content": "import 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\n\ntypedef ContextMenuBuilder = List<ContextMenuButtonItem> Function();\n\n/// 根据用户手势显示和隐藏上下文菜单。\n/// 默认情况下，在右键单击和长按时显示菜单。\nclass ContextMenuRegion extends StatefulWidget {\n  const ContextMenuRegion({\n    super.key,\n    required this.child,\n    required this.contextMenuBuilder,\n  });\n\n  /// Builds the context menu.\n  final ContextMenuBuilder contextMenuBuilder;\n\n  /// The child widget that will be listened to for gestures.\n  final Widget child;\n\n  @override\n  State<ContextMenuRegion> createState() => _ContextMenuRegionState();\n}\n\nclass _ContextMenuRegionState extends State<ContextMenuRegion> {\n  Offset? _longPressOffset;\n\n  final ContextMenuController _contextMenuController = ContextMenuController();\n\n  static bool get _longPressEnabled {\n    switch (defaultTargetPlatform) {\n      case TargetPlatform.android:\n      case TargetPlatform.iOS:\n        return true;\n      case TargetPlatform.macOS:\n      case TargetPlatform.fuchsia:\n      case TargetPlatform.linux:\n      case TargetPlatform.windows:\n        return false;\n    }\n  }\n\n  void _onSecondaryTapUp(TapUpDetails details) {\n    _show(details.globalPosition);\n  }\n\n  void _onTap() {\n    if (!_contextMenuController.isShown) {\n      return;\n    }\n    _hide();\n  }\n\n  void _onLongPressStart(LongPressStartDetails details) {\n    _longPressOffset = details.globalPosition;\n  }\n\n  void _onLongPress() {\n    assert(_longPressOffset != null);\n    _show(_longPressOffset!);\n    _longPressOffset = null;\n  }\n\n  void _show(Offset position) {\n    _contextMenuController.show(\n      context: context,\n      contextMenuBuilder: (context) {\n        return AdaptiveTextSelectionToolbar.buttonItems(\n            buttonItems: widget.contextMenuBuilder.call(),\n            anchors: TextSelectionToolbarAnchors(primaryAnchor: position));\n      },\n    );\n  }\n\n  void _hide() {\n    _contextMenuController.remove();\n  }\n\n  @override\n  void dispose() {\n    _hide();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onSecondaryTapUp: _onSecondaryTapUp,\n      onTap: _onTap,\n      onLongPress: _longPressEnabled ? _onLongPress : null,\n      onLongPressStart: _longPressEnabled ? _onLongPressStart : null,\n      child: widget.child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/device.dart",
    "content": "import 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:device_info_plus/device_info_plus.dart';\n\nclass DeviceUtils {\n  /// Get the device id\n  static Future<String?> deviceId() async {\n    var deviceInfoPlugin = DeviceInfoPlugin();\n    if (Platform.isAndroid) {\n      return deviceInfoPlugin.androidInfo.then((it) => it.id);\n    } else if (Platform.isIOS) {\n      return deviceInfoPlugin.iosInfo.then((it) => it.identifierForVendor);\n    }\n\n    return await DesktopMultiWindow.invokeMethod(0, \"deviceId\", null);\n  }\n\n  /// Get the desktop device id\n  static Future<String?> desktopDeviceId() async {\n    var deviceInfoPlugin = DeviceInfoPlugin();\n    if (Platform.isWindows) {\n      return deviceInfoPlugin.windowsInfo.then((it) => it.deviceId);\n    } else if (Platform.isMacOS) {\n      return deviceInfoPlugin.macOsInfo.then((it) => it.systemGUID);\n    } else if (Platform.isLinux) {\n      return deviceInfoPlugin.linuxInfo.then((it) => it.machineId);\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/history_cache_time.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\n\n///缓存时间菜单\n/// @author wanghongen\nclass HistoryCacheTime extends StatefulWidget {\n  final Configuration configuration;\n  final Function(int) onSelected;\n\n  const HistoryCacheTime(this.configuration, {super.key, required this.onSelected});\n\n  @override\n  State<StatefulWidget> createState() => _HistoryCacheTimeState();\n}\n\nclass _HistoryCacheTimeState extends State<HistoryCacheTime> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return PopupMenuButton(\n        tooltip: localizations.historyCacheTime,\n        offset: const Offset(0, 35),\n        icon: const Icon(Icons.av_timer, size: 19),\n        initialValue: widget.configuration.historyCacheTime,\n        constraints: const BoxConstraints(minWidth: 34, minHeight: 34),\n        onSelected: (val) {\n          widget.configuration.historyCacheTime = val;\n          widget.configuration.flushConfig();\n          setState(() {\n            widget.onSelected.call(val);\n          });\n        },\n        itemBuilder: (BuildContext context) {\n          return [\n            PopupMenuItem(value: 0, height: 35, child: Text(localizations.historyManualSave)),\n            PopupMenuItem(value: 7, height: 35, child: Text(localizations.historyDay(7))),\n            PopupMenuItem(value: 30, height: 35, child: Text(localizations.historyDay(30))),\n            PopupMenuItem(value: 99999, height: 35, child: Text(localizations.historyForever)),\n          ];\n        });\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/http_method_popup.dart",
    "content": "// Add MethodPopupMenu widget for compact colored method display\nimport 'package:flutter/material.dart';\n\nimport '../../network/http/http.dart';\n\nclass MethodPopupMenu extends StatelessWidget {\n  final HttpMethod? value;\n  final ValueChanged<HttpMethod?> onChanged;\n  final bool showSeparator; // whether to display the vertical separator to the right\n\n  const MethodPopupMenu({super.key, required this.value, required this.onChanged, this.showSeparator = true});\n\n  Color _methodColor(HttpMethod? m, BuildContext context) {\n    // colors chosen similar to Postman style\n    switch (m) {\n      case HttpMethod.get:\n        return Colors.green.shade700;\n      case HttpMethod.post:\n        return Colors.orange.shade700;\n      case HttpMethod.put:\n        return Colors.blue.shade700;\n      case HttpMethod.patch:\n        return Colors.purple.shade700;\n      case HttpMethod.delete:\n        return Colors.red.shade700;\n      case HttpMethod.options:\n        return Colors.teal.shade700; // OPTIONS colored teal\n      case HttpMethod.head:\n        return Colors.indigo.shade700; // HEAD colored indigo\n      case HttpMethod.trace:\n      case HttpMethod.connect:\n      case HttpMethod.propfind:\n      case HttpMethod.report:\n        return Colors.grey.shade700;\n      default:\n        return Colors.grey.shade700;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var items = <DropdownMenuItem<HttpMethod?>>[];\n    items.add(DropdownMenuItem<HttpMethod?>(value: null, child: _buildMenuItem(null, context)));\n    for (var m in HttpMethod.methods()) {\n      if (m == HttpMethod.connect || m == HttpMethod.options) continue;\n      items.add(DropdownMenuItem<HttpMethod?>(value: m, child: _buildMenuItem(m, context)));\n    }\n\n    final dropdown = DropdownButton<HttpMethod?>(\n      padding: const EdgeInsets.only(),\n      alignment: AlignmentDirectional.center,\n      isDense: true,\n      focusColor: Colors.transparent,\n      underline: const SizedBox(),\n      value: value,\n      onChanged: onChanged,\n      items: items,\n    );\n\n    // render dropdown and optional separator together so caller doesn't need to add one\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        dropdown,\n        if (showSeparator) ...[\n          const SizedBox(width: 3),\n          Container(width: 1, height: 22, color: Colors.grey.shade300),\n          const SizedBox(width: 3),\n        ]\n      ],\n    );\n  }\n\n  Widget _buildMenuItem(HttpMethod? m, BuildContext context) {\n    final name = m == null ? 'ANY' : m.name;\n    final color = _methodColor(m, context);\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 4),\n      child: Text(name, style: TextStyle(color: color, fontSize: 13, fontWeight: FontWeight.w600)),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/json/json_text.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:io';\nimport 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/json/theme.dart';\nimport 'package:proxypin/ui/component/search/search_controller.dart';\nimport 'package:proxypin/utils/font.dart';\nimport 'package:scrollable_positioned_list_nic/scrollable_positioned_list_nic.dart';\n\nimport '../../../utils/platform.dart';\n\nclass JsonText extends StatefulWidget {\n  final ColorTheme colorTheme;\n  final dynamic json;\n  final String indent;\n  final ScrollController? scrollController;\n  final SearchTextController? searchController;\n\n  const JsonText({\n    super.key,\n    required this.json,\n    this.indent = '  ',\n    required this.colorTheme,\n    this.scrollController,\n    this.searchController,\n  });\n\n  @override\n  State<JsonText> createState() => _JsonTextState();\n}\n\nclass _JsonTextState extends State<JsonText> {\n  ScrollController? trackingScrollController;\n  SearchTextController? searchController;\n  final ItemScrollController itemScrollController = ItemScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    searchController = widget.searchController;\n  }\n\n  @override\n  void dispose() {\n    trackingScrollController?.dispose();\n    trackingScrollController = null;\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (searchController == null) {\n      return jsonTextWidget(context);\n    }\n    return AnimatedBuilder(\n      animation: searchController!,\n      builder: (context, child) {\n        return jsonTextWidget(context);\n      },\n    );\n  }\n\n  Widget jsonTextWidget(BuildContext context) {\n    var jsonParser = JsonParser(widget.json, widget.colorTheme, widget.indent, searchController);\n    var textList = jsonParser.getJsonTree();\n    List<List<TextSpan>>? chunks;\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      searchController?.updateMatchCount(jsonParser.searchMatchTotal);\n      // 自动滚动到当前高亮项\n      scrollToMatch(jsonParser, chunks);\n    });\n\n    if (textList.length < 1000) {\n      return SelectableText.rich(TextSpan(children: textList), showCursor: true);\n    } else {\n      chunks = chunks ?? splitTextSpans(textList, 500);\n      return SizedBox(\n          width: double.infinity,\n          height: MediaQuery.of(context).size.height - 200,\n          child: SelectionArea(\n              child: ScrollablePositionedList.builder(\n            physics: Platforms.isDesktop() ? null : const BouncingScrollPhysics(),\n            scrollController: Platforms.isDesktop() ? null : trackingScroll(),\n            itemCount: chunks.length,\n            minCacheExtent: 1500,\n            itemScrollController: itemScrollController,\n            itemBuilder: (BuildContext context, int index) {\n              return Text.rich(\n                TextSpan(children: chunks![index]),\n                textHeightBehavior:\n                    const TextHeightBehavior(applyHeightToFirstAscent: false, applyHeightToLastDescent: false),\n                strutStyle: const StrutStyle(forceStrutHeight: true, height: 1.393),\n                style: TextStyle(fontFamily: fonts.regular),\n              );\n            },\n          )));\n    }\n  }\n\n  Future<void> scrollToMatch(JsonParser jsonParser, [List<List<TextSpan>>? chunks]) async {\n    if (searchController == null || jsonParser.matchKeys.isEmpty) return;\n    final index = searchController!.currentMatchIndex.value;\n    if (index < 0 || index >= jsonParser.matchKeys.length) return;\n\n    final key = jsonParser.matchKeys[index];\n\n    if (key.currentContext != null) {\n      await _ensureVisibleCenter(key, const Duration(milliseconds: 260));\n      return;\n    }\n\n    // Chunk-first path for large documents\n    if (chunks != null && chunks.isNotEmpty) {\n      final chunkIndex = _findChunkIndexForKey(chunks, key);\n      if (chunkIndex != -1) {\n        /// 滚动到对应 chunk\n        try {\n          await itemScrollController.scrollTo(\n            index: chunkIndex,\n            duration: const Duration(milliseconds: 150),\n            curve: Curves.easeOut,\n            alignment: 0.0,\n          );\n        } catch (_) {\n          logger.w('Scroll to chunk $chunkIndex failed');\n        }\n\n        for (int i = 0; i < 10 && key.currentContext == null; i++) {\n          await Future.delayed(Duration(milliseconds: 40));\n        }\n\n        await _ensureVisibleCenter(key, const Duration(milliseconds: 130));\n        return;\n      }\n    }\n  }\n\n  Future<void> _ensureVisibleCenter(GlobalKey key, Duration duration) async {\n    final ctx = key.currentContext;\n    if (ctx != null) {\n      await Scrollable.ensureVisible(ctx, duration: duration, alignment: 0.5);\n    }\n  }\n\n  // 在分块数据中定位包含目标 key 的 chunk 下标\n  int _findChunkIndexForKey(List<List<TextSpan>> chunks, GlobalKey key) {\n    for (int i = 0; i < chunks.length; i++) {\n      for (final span in chunks[i]) {\n        if (_textSpanContainsKey(span, key)) return i;\n      }\n    }\n    return -1;\n  }\n\n  // 递归检查 TextSpan 树是否包含对应 key 的 WidgetSpan\n  bool _textSpanContainsKey(TextSpan span, GlobalKey key) {\n    final children = span.children;\n    if (children == null || children.isEmpty) return false;\n    for (final child in children) {\n      if (child is WidgetSpan) {\n        final w = child.child;\n        if (w is Text && w.key == key) return true;\n      } else if (child is TextSpan) {\n        if (_textSpanContainsKey(child, key)) return true;\n      }\n    }\n    return false;\n  }\n\n  // 优化分块：避免因为 Text 组件分隔导致额外空行\n  List<List<TextSpan>> splitTextSpans(List<TextSpan> spans, int chunkSize) {\n    if (spans.length <= chunkSize) {\n      return [spans];\n    }\n\n    List<List<TextSpan>> chunks = [];\n\n    bool endsWithNewline(TextSpan s) => s.text != null && s.text!.endsWith('\\n');\n    bool startsWithNewline(TextSpan s) => s.text != null && s.text!.startsWith('\\n');\n\n    for (int i = 0; i < spans.length; i += chunkSize) {\n      final chunk = spans.sublist(i, (i + chunkSize < spans.length) ? i + chunkSize : spans.length);\n\n      if (chunk.isEmpty) continue;\n\n      if (i > 0) {\n        // 对非首块：去掉首 span 的一个前导换行抵消组件分隔换行\n        final first = chunk.first;\n        if (startsWithNewline(first)) {\n          final newText = first.text!.substring(1);\n          chunk[0] = TextSpan(text: newText, style: first.style, children: first.children);\n        }\n      }\n\n      if (chunk.length > 1 && endsWithNewline(chunk.last)) {\n        // 除最后一块外，块尾不保留以 \\n 结尾的 span（把它挪到下一块）\n        final last = chunk.last;\n        final newText = last.text!.substring(0, last.text!.length - 1);\n        chunk[chunk.length - 1] = TextSpan(text: newText, style: last.style, children: last.children);\n      }\n\n      chunks.add(chunk);\n    }\n    return chunks;\n  }\n\n  /// 滚动条控制：保证 ListView/SingleChildScrollView 使用同一个控制器，便于动画\n  ScrollController trackingScroll() {\n    if (trackingScrollController != null) {\n      return trackingScrollController!;\n    }\n\n    var trackingScroll = TrackingScrollController();\n    ScrollController? scrollController = widget.scrollController;\n\n    double prevOffset = 0;\n    trackingScroll.addListener(() {\n      // iOS 回弹或向上轻微滑动时，驱动外部滚动条联动\n      if (trackingScroll.offset < -10 || (trackingScroll.offset < 30 && trackingScroll.offset < prevOffset)) {\n        if (scrollController != null && scrollController.offset >= 50) {\n          scrollController.jumpTo(scrollController.offset - max((prevOffset - trackingScroll.offset), 10));\n        }\n      }\n      prevOffset = trackingScroll.offset;\n    });\n\n    if (Platform.isIOS && scrollController != null) {\n      scrollController.addListener(() {\n        if (scrollController.offset >= scrollController.position.maxScrollExtent) {\n          scrollController.jumpTo(scrollController.position.maxScrollExtent);\n          trackingScroll\n              .jumpTo(trackingScroll.offset + (scrollController.offset - scrollController.position.maxScrollExtent));\n        }\n      });\n    }\n\n    trackingScrollController = trackingScroll;\n    return trackingScroll;\n  }\n}\n\nclass JsonParser {\n  final dynamic json;\n  final ColorTheme colorTheme;\n  final String indent;\n  final SearchTextController? searchController;\n  int searchMatchTotal = 0;\n  final List<GlobalKey> matchKeys = [];\n\n  JsonParser(this.json, this.colorTheme, this.indent, this.searchController);\n\n  int getLength() {\n    if (json is Map) {\n      return json.length;\n    } else if (json is List) {\n      return json.length;\n    } else {\n      return json == null ? 0 : json.toString().length;\n    }\n  }\n\n  List<TextSpan> getJsonTree() {\n    matchKeys.clear(); // 每次渲染前清空\n    List<TextSpan> textList = [];\n    if (json is Map) {\n      textList.add(const TextSpan(text: '{ \\n'));\n      textList.addAll(getMapText(json, prefix: indent));\n    } else if (json is List) {\n      textList.add(const TextSpan(text: '[ \\n'));\n      textList.addAll(getArrayText(json));\n    } else {\n      textList.add(TextSpan(text: json == null ? '' : json.toString()));\n      textList.add(const TextSpan(text: '\\n'));\n    }\n    return textList;\n  }\n\n  /// 获取Map json\n  List<TextSpan> getMapText(Map<String, dynamic> map,\n      {String openPrefix = '', String prefix = '', String suffix = ''}) {\n    var result = <TextSpan>[];\n\n    var entries = map.entries;\n    for (int i = 0; i < entries.length; i++) {\n      var entry = entries.elementAt(i);\n      String postfix = '${i == entries.length - 1 ? '' : ','} ';\n\n      var textSpan = TextSpan(text: prefix, children: [\n        ..._highlightMatches('\"${entry.key}\"', textColor: colorTheme.propertyKey),\n        const TextSpan(text: ': '),\n        getBasicValue(entry.value, postfix),\n      ]);\n      result.add(textSpan);\n      result.add(const TextSpan(text: '\\n'));\n\n      if (entry.value is Map<String, dynamic>) {\n        result.addAll(getMapText(entry.value, openPrefix: prefix, prefix: '$prefix$indent', suffix: postfix));\n      } else if (entry.value is List) {\n        result.addAll(getArrayText(entry.value, openPrefix: prefix, prefix: '$prefix$indent', suffix: postfix));\n      }\n    }\n\n    result.add(TextSpan(text: '$openPrefix}$suffix  \\n'));\n    return result;\n  }\n\n  /// 获取数组json\n  List<TextSpan> getArrayText(List<dynamic> list, {String openPrefix = '', String prefix = '', String suffix = ''}) {\n    var result = <TextSpan>[];\n    // result.add(TextSpan(text: '$openPrefix[ \\n'));\n\n    for (int i = 0; i < list.length; i++) {\n      var value = list[i];\n      String postfix = i == list.length - 1 ? '' : ',';\n\n      result.add(getBasicValue(value, postfix, prefix: prefix));\n      result.add(const TextSpan(text: '\\n'));\n\n      if (value is Map<String, dynamic>) {\n        result.addAll(getMapText(value, openPrefix: '$openPrefix ', prefix: '$prefix$indent', suffix: postfix));\n      } else if (value is List) {\n        result.addAll(getArrayText(value, openPrefix: '$openPrefix ', prefix: '$prefix$indent', suffix: postfix));\n      }\n    }\n\n    result.add(TextSpan(text: '$openPrefix]$suffix \\n'));\n    return result;\n  }\n\n  /// 获取基本类型值 复杂类型会忽略\n  TextSpan getBasicValue(dynamic value, String suffix, {String? prefix}) {\n    if (value == null) {\n      return TextSpan(\n          text: prefix,\n          children: [..._highlightMatches('null', textColor: colorTheme.keyword), TextSpan(text: suffix)]);\n    }\n\n    if (value is String) {\n      return TextSpan(\n          text: prefix,\n          children: [..._highlightMatches('\"$value\"', textColor: colorTheme.string), TextSpan(text: suffix)]);\n    }\n\n    if (value is num) {\n      return TextSpan(\n          text: prefix,\n          children: [..._highlightMatches(value.toString(), textColor: colorTheme.number), TextSpan(text: suffix)]);\n    }\n\n    if (value is bool) {\n      return TextSpan(\n          text: prefix,\n          children: [..._highlightMatches(value.toString(), textColor: colorTheme.keyword), TextSpan(text: suffix)]);\n    }\n\n    if (value is List) {\n      return TextSpan(children: _highlightMatches(\"${prefix ?? ''}[\"));\n    }\n\n    return TextSpan(children: _highlightMatches(\"${prefix ?? ''}{\"));\n  }\n\n  List<InlineSpan> _highlightMatches(String text, {Color? textColor}) {\n    if (searchController == null || searchController?.shouldSearch() == false) {\n      return [TextSpan(text: text, style: TextStyle(color: textColor))];\n    }\n\n    final pattern = searchController!.value.pattern;\n    final regex = searchController!.value.isRegExp\n        ? RegExp(pattern, caseSensitive: searchController!.value.isCaseSensitive)\n        : RegExp(RegExp.escape(pattern), caseSensitive: searchController!.value.isCaseSensitive);\n\n    final spans = <InlineSpan>[];\n    int start = 0;\n    var allMatches = regex.allMatches(text).toList();\n    final currentIndex = searchController!.currentMatchIndex.value;\n    for (int i = 0; i < allMatches.length; i++) {\n      final match = allMatches[i];\n      if (match.start > start) {\n        spans.add(TextSpan(text: text.substring(start, match.start), style: TextStyle(color: textColor)));\n      }\n      // 为每个高亮项分配一个 GlobalKey\n      final key = GlobalKey();\n      matchKeys.add(key);\n\n      spans.add(WidgetSpan(\n        alignment: PlaceholderAlignment.middle,\n        baseline: TextBaseline.ideographic,\n        child: Text(\n          text.substring(match.start, match.end),\n          key: key,\n          style: TextStyle(\n            color: textColor,\n            backgroundColor:\n                searchMatchTotal == currentIndex ? colorTheme.searchMatchCurrentColor : colorTheme.searchMatchColor,\n          ),\n        ),\n      ));\n      start = match.end;\n      searchMatchTotal += 1; // 统计总匹配数\n    }\n\n    if (start < text.length) {\n      spans.add(TextSpan(text: text.substring(start), style: TextStyle(color: textColor)));\n    }\n    return spans;\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/json/json_viewer.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/ui/component/json/theme.dart';\nimport 'package:proxypin/ui/component/json/toast.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/platform.dart';\n\nimport '../search/search_controller.dart';\n\nclass JsonViewer extends StatelessWidget {\n  final dynamic jsonObj;\n  final ColorTheme colorTheme;\n  final SearchTextController? searchController;\n\n  const JsonViewer(this.jsonObj, {super.key, required this.colorTheme, this.searchController});\n\n  @override\n  Widget build(BuildContext context) {\n    final matchKeys = <GlobalKey>[];\n    if (searchController == null) {\n      return DefaultTextStyle.merge(\n          style: const TextStyle(fontWeight: FontWeight.w600),\n          child: getContentWidget(jsonObj, matchTotalCount: ValueWrap.of(0), matchKeys: matchKeys));\n    }\n    return AnimatedBuilder(\n        animation: searchController ?? ValueNotifier(0),\n        builder: (context, child) {\n          final matchTotalCount = ValueWrap.of(0);\n          matchKeys.clear();\n          final contentWidget = DefaultTextStyle.merge(\n              style: const TextStyle(fontWeight: FontWeight.w600),\n              child: getContentWidget(jsonObj, matchTotalCount: matchTotalCount, matchKeys: matchKeys));\n          WidgetsBinding.instance.addPostFrameCallback((_) {\n            searchController?.updateMatchCount(matchTotalCount.get()!);\n            scrollToMatch(matchKeys);\n          });\n          return contentWidget;\n        });\n  }\n\n  Widget getContentWidget(dynamic content,\n      {required ValueWrap<int> matchTotalCount, required List<GlobalKey> matchKeys}) {\n    if (content is List) {\n      return JsonArrayViewer(content,\n          colorTheme: colorTheme,\n          searchController: searchController,\n          matchTotalCount: matchTotalCount,\n          matchKeys: matchKeys);\n    } else if (content is Map<String, dynamic>) {\n      return JsonObjectViewer(content,\n          colorTheme: colorTheme,\n          searchController: searchController,\n          matchTotalCount: matchTotalCount,\n          matchKeys: matchKeys);\n    } else {\n      return SelectableText(showCursor: true, content?.toString() ?? '');\n    }\n  }\n\n  void scrollToMatch(List<GlobalKey> matchKeys) {\n    if (searchController != null && matchKeys.isNotEmpty) {\n      final currentIndex = searchController!.currentMatchIndex.value;\n      if (currentIndex >= 0 && currentIndex < matchKeys.length) {\n        final key = matchKeys[currentIndex];\n        final context = key.currentContext;\n        if (context != null) {\n          Scrollable.ensureVisible(\n            context,\n            duration: const Duration(milliseconds: 300),\n            alignment: 0.5, // 高亮项在视图中的位置\n          );\n        }\n      }\n    }\n  }\n}\n\nclass JsonObjectViewer extends StatefulWidget {\n  final ColorTheme colorTheme;\n  final Map<String, dynamic> jsonObj;\n  final bool notRoot;\n  final SearchTextController? searchController;\n  final ValueWrap<int> matchTotalCount;\n  final List<GlobalKey> matchKeys;\n\n  const JsonObjectViewer(this.jsonObj,\n      {super.key,\n      this.notRoot = false,\n      required this.colorTheme,\n      this.searchController,\n      required this.matchTotalCount,\n      required this.matchKeys});\n\n  @override\n  JsonObjectViewerState createState() => JsonObjectViewerState();\n}\n\nclass JsonObjectViewerState extends State<JsonObjectViewer> {\n  Map<String, bool> openFlag = {};\n\n  @override\n  void didUpdateWidget(covariant JsonObjectViewer oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    openFlag = {};\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.notRoot) {\n      return Container(\n        padding: const EdgeInsets.only(left: 14.0),\n        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: _getList()),\n      );\n    }\n    return Column(crossAxisAlignment: CrossAxisAlignment.start, children: _getList());\n  }\n\n  List<Widget> _getList() {\n    List<Widget> list = [];\n    for (MapEntry entry in widget.jsonObj.entries) {\n      if (openFlag[entry.key] == null) {\n        openFlag[entry.key] = widget.notRoot == false && _isExtensible(entry.value);\n      }\n\n      list.add(Row(\n        children: <Widget>[\n          getKeyWidget(entry),\n          Text(':', style: TextStyle(color: widget.colorTheme.colon)),\n          const SizedBox(width: 3),\n          _copyValue(\n              context,\n              _getValueWidget(entry.value, widget.colorTheme,\n                  searchController: widget.searchController,\n                  matchTotalCount: widget.matchTotalCount,\n                  matchKeys: widget.matchKeys),\n              entry.value),\n        ],\n      ));\n      list.add(const SizedBox(height: 4));\n\n      if ((openFlag[entry.key] ?? false) && entry.value != null) {\n        list.add(getContentWidget(entry.value, widget.colorTheme,\n            searchController: widget.searchController,\n            matchTotalCount: widget.matchTotalCount,\n            matchKeys: widget.matchKeys));\n      }\n    }\n    return list;\n  }\n\n  // key\n  Widget getKeyWidget(MapEntry entry) {\n    final keyText = entry.key;\n\n    final keyWidget = Container(\n        constraints: BoxConstraints(maxWidth: 350),\n        child: SelectableText.rich(\n            showCursor: true,\n            TextSpan(\n                children: _highlightText(keyText, TextStyle(color: widget.colorTheme.propertyKey),\n                    searchController: widget.searchController,\n                    colorTheme: widget.colorTheme,\n                    matchTotalCount: widget.matchTotalCount,\n                    matchKeys: widget.matchKeys))));\n\n    //是否有子层级\n    if (_isExtensible(entry.value)) {\n      return InkWell(\n          onTap: () {\n            setState(() {\n              openFlag[entry.key] = !(openFlag[entry.key] ?? false);\n            });\n          },\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.start,\n            children: [\n              (openFlag[entry.key] ?? false)\n                  ? const Icon(Icons.keyboard_arrow_down, size: 18)\n                  : const Icon(Icons.keyboard_arrow_right, size: 18),\n              keyWidget,\n            ],\n          ));\n    }\n\n    return Row(children: [\n      const Icon(Icons.keyboard_arrow_right, color: Color.fromARGB(0, 0, 0, 0), size: 18),\n      keyWidget,\n    ]);\n  }\n\n  static Widget getContentWidget(dynamic content, ColorTheme colorTheme,\n      {SearchTextController? searchController,\n      required ValueWrap<int> matchTotalCount,\n      required List<GlobalKey> matchKeys}) {\n    if (content is List) {\n      return JsonArrayViewer(content,\n          notRoot: true,\n          colorTheme: colorTheme,\n          searchController: searchController,\n          matchTotalCount: matchTotalCount,\n          matchKeys: matchKeys);\n    } else {\n      return JsonObjectViewer(content,\n          notRoot: true,\n          colorTheme: colorTheme,\n          searchController: searchController,\n          matchTotalCount: matchTotalCount,\n          matchKeys: matchKeys);\n    }\n  }\n}\n\nclass JsonArrayViewer extends StatefulWidget {\n  final ColorTheme colorTheme;\n  final List<dynamic> jsonArray;\n  final bool notRoot;\n  final SearchTextController? searchController;\n  final ValueWrap<int> matchTotalCount;\n  final List<GlobalKey> matchKeys;\n\n  const JsonArrayViewer(this.jsonArray,\n      {super.key,\n      this.notRoot = false,\n      required this.colorTheme,\n      this.searchController,\n      required this.matchTotalCount,\n      required this.matchKeys});\n\n  @override\n  State<JsonArrayViewer> createState() => _JsonArrayViewerState();\n}\n\nclass _JsonArrayViewerState extends State<JsonArrayViewer> {\n  late List<bool> openFlag;\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.notRoot) {\n      return Container(\n          padding: const EdgeInsets.only(left: 14.0),\n          child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: _getList()));\n    }\n    return Column(crossAxisAlignment: CrossAxisAlignment.start, children: _getList());\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    openFlag = List.filled(widget.jsonArray.length, false);\n  }\n\n  List<Widget> _getList() {\n    List<Widget> list = [];\n    int i = 0;\n    for (dynamic content in widget.jsonArray) {\n      list.add(Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: <Widget>[\n          getKeyWidget(content, i),\n          Text(':', style: TextStyle(color: widget.colorTheme.colon)),\n          const SizedBox(width: 3),\n          _copyValue(\n              context,\n              _getValueWidget(content, widget.colorTheme,\n                  searchController: widget.searchController,\n                  matchTotalCount: widget.matchTotalCount,\n                  matchKeys: widget.matchKeys),\n              content)\n        ],\n      ));\n      list.add(const SizedBox(height: 4));\n      if (openFlag[i]) {\n        list.add(JsonObjectViewerState.getContentWidget(content, widget.colorTheme,\n            searchController: widget.searchController,\n            matchTotalCount: widget.matchTotalCount,\n            matchKeys: widget.matchKeys));\n      }\n      i++;\n    }\n    return list;\n  }\n\n  // key\n  Widget getKeyWidget(dynamic content, int index) {\n    //是否有子层级\n    if (_isExtensible(content)) {\n      return InkWell(\n          onTap: () {\n            setState(() {\n              openFlag[index] = !(openFlag[index]);\n            });\n          },\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.start,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              openFlag[index]\n                  ? const Icon(Icons.keyboard_arrow_down, size: 18)\n                  : const Icon(Icons.keyboard_arrow_right, size: 18),\n              Text('[$index]', style: TextStyle(color: widget.colorTheme.propertyKey)),\n            ],\n          ));\n    }\n\n    return Row(children: [\n      const Icon(Icons.arrow_right, color: Color.fromARGB(0, 0, 0, 0), size: 18),\n      Text('[$index]', style: TextStyle(color: widget.colorTheme.propertyKey)),\n    ]);\n  }\n}\n\nWidget _getValueWidget(dynamic value, ColorTheme colorTheme,\n    {SearchTextController? searchController,\n    required ValueWrap<int> matchTotalCount,\n    required List<GlobalKey> matchKeys}) {\n  String valueStr;\n  TextStyle style;\n  if (value == null) {\n    valueStr = 'null';\n    style = TextStyle(color: colorTheme.keyword);\n  } else if (value is num) {\n    valueStr = value.toString();\n    style = TextStyle(color: colorTheme.number);\n  } else if (value is String) {\n    valueStr = '\"$value\"';\n    style = TextStyle(color: colorTheme.string);\n  } else if (value is bool) {\n    valueStr = value.toString();\n    style = TextStyle(color: colorTheme.keyword);\n  } else if (value is List) {\n    if (value.isEmpty) {\n      valueStr = 'Array[0]';\n      style = const TextStyle();\n    } else {\n      valueStr = 'Array<${_getTypeName(value[0])}>[${value.length}]';\n      style = const TextStyle();\n    }\n  } else {\n    valueStr = 'Object';\n    style = const TextStyle(fontSize: 13);\n  }\n\n  if (searchController?.shouldSearch() == true) {\n    return SelectableText.rich(\n      showCursor: true,\n      TextSpan(\n          children: _highlightText(valueStr, style,\n              searchController: searchController,\n              colorTheme: colorTheme,\n              matchTotalCount: matchTotalCount,\n              matchKeys: matchKeys)),\n    );\n  }\n\n  return SelectableText(showCursor: true, valueStr, style: style);\n}\n\nList<InlineSpan> _highlightText(String text, TextStyle textStyle,\n    {SearchTextController? searchController,\n    required ColorTheme colorTheme,\n    required ValueWrap<int> matchTotalCount,\n    required List<GlobalKey> matchKeys}) {\n  if (searchController == null || searchController.shouldSearch() == false) {\n    return [TextSpan(text: text, style: textStyle)];\n  }\n\n  final pattern = searchController.value.pattern;\n  final regex = searchController.value.isRegExp\n      ? RegExp(pattern, caseSensitive: searchController.value.isCaseSensitive)\n      : RegExp(RegExp.escape(pattern), caseSensitive: searchController.value.isCaseSensitive);\n\n  final spans = <InlineSpan>[];\n  int start = 0;\n  var allMatches = regex.allMatches(text).toList();\n  final currentIndex = searchController.currentMatchIndex.value;\n  for (int i = 0; i < allMatches.length; i++) {\n    final match = allMatches[i];\n    if (match.start > start) {\n      spans.add(TextSpan(text: text.substring(start, match.start), style: textStyle));\n    }\n    // 为每个高亮项分配一个 GlobalKey\n    final key = GlobalKey();\n    matchKeys.add(key);\n    spans.add(WidgetSpan(\n      alignment: PlaceholderAlignment.middle,\n      baseline: TextBaseline.ideographic,\n      child: Text(\n          key: key,\n          text.substring(match.start, match.end),\n          style: textStyle.copyWith(\n            backgroundColor: matchTotalCount.get() == currentIndex\n                ? colorTheme.searchMatchCurrentColor\n                : colorTheme.searchMatchColor,\n          )),\n    ));\n    start = match.end;\n\n    matchTotalCount.set(matchTotalCount.get()! + 1);\n  }\n\n  if (start < text.length) {\n    spans.add(TextSpan(text: text.substring(start), style: textStyle));\n  }\n  return spans;\n}\n\n///获取值的类型\nString _getTypeName(dynamic content) {\n  if (content is int) {\n    return 'int';\n  } else if (content is String) {\n    return 'String';\n  } else if (content is bool) {\n    return 'bool';\n  } else if (content is double) {\n    return 'double';\n  } else if (content is List) {\n    return 'List';\n  }\n  return 'Object';\n}\n\n/// 复制值\nWidget _copyValue(BuildContext context, Widget child, Object? value) {\n  return Flexible(\n      child: GestureDetector(\n          onSecondaryTapDown: (details) => showJsonCopyMenu(context, details.globalPosition, value),\n          onTapDown:\n              Platforms.isDesktop() ? null : (details) => showJsonCopyMenu(context, details.globalPosition, value),\n          child: child));\n}\n\nvoid showJsonCopyMenu(BuildContext context, Offset position, Object? value) {\n  AppLocalizations localizations = AppLocalizations.of(context)!;\n\n  //显示复制菜单\n  showMenu(\n      context: context,\n      position: RelativeRect.fromLTRB(position.dx, position.dy, position.dx, position.dy),\n      items: [\n        PopupMenuItem(\n            height: 30,\n            child: Text(localizations.copy),\n            onTap: () {\n              if (value == null) {\n                return;\n              }\n              Clipboard.setData(ClipboardData(text: value is String ? value : jsonEncode(value)))\n                  .then((value) => Toast.show(localizations.copied, context));\n            })\n      ]);\n}\n\n/// 是否可展开\nbool _isExtensible(dynamic content) {\n  if (content == null) {\n    return false;\n  } else if (content is int) {\n    return false;\n  } else if (content is String) {\n    return false;\n  } else if (content is bool) {\n    return false;\n  } else if (content is double) {\n    return false;\n  }\n  return true;\n}\n"
  },
  {
    "path": "lib/ui/component/json/theme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ColorTheme {\n  static ColorTheme light(ColorScheme colorScheme) => ColorTheme(\n        background: const Color(0xffffffff),\n        propertyKey: const Color(0xff871094),\n        colon: Colors.black,\n        string: const Color(0xff067d17),\n        number: const Color(0xff1750eb),\n        keyword: const Color(0xff0033b3),\n        searchMatchColor: colorScheme.inversePrimary,\n        searchMatchCurrentColor: colorScheme.primary,\n      );\n\n  static ColorTheme dark(ColorScheme colorScheme) => ColorTheme(\n        background: const Color(0XFF1E1F22),\n        propertyKey: const Color(0XFFC77DBB),\n        colon: const Color(0XFFBCBEC4),\n        string: const Color(0XFF6AAB73),\n        number: const Color(0XFF2AACB8),\n        keyword: const Color(0XFFCF8E6D),\n        searchMatchColor: colorScheme.inversePrimary,\n        searchMatchCurrentColor: colorScheme.primary,\n      );\n\n  final Color background;\n  final Color propertyKey;\n  final Color colon;\n  final Color string;\n  final Color number;\n  final Color keyword;\n  final Color? searchMatchColor;\n  final Color? searchMatchCurrentColor;\n\n  const ColorTheme({\n    required this.background,\n    required this.propertyKey,\n    required this.colon,\n    required this.string,\n    required this.number,\n    required this.keyword,\n    required this.searchMatchColor,\n    required this.searchMatchCurrentColor,\n  });\n\n  static ColorTheme of(BuildContext context) {\n    final colorScheme = Theme.of(context).colorScheme;\n    final brightness = Theme.of(context).brightness;\n    return brightness == Brightness.dark ? ColorTheme.dark(colorScheme) : ColorTheme.light(colorScheme);\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/json/toast.dart",
    "content": "import 'package:flutter/cupertino.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\n\nclass Toast {\n  static void show(String message, BuildContext context) {\n    FlutterToastr.show(message, context);\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/memory_cleanup.dart",
    "content": "/*\n * Copyright 2024 hongen Wang All rights reserved.\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\nimport 'dart:io';\n\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/configuration.dart';\n\n/// Memory cleanup handle\n/// @author wanghongen\n\nclass MemoryCleanupMonitor {\n  static bool _processing = false;\n\n  static void onMonitor({Function? onCleanup}) {\n    var threshold = AppConfiguration.current?.memoryCleanupThreshold;\n    if (threshold == null || threshold <= 0) {\n      return;\n    }\n\n    if (_processing) return;\n    _processing = true;\n    Future.delayed(const Duration(seconds: 3), () {\n      _processing = false;\n      _cleanup(threshold, onCleanup);\n    });\n  }\n\n  static void _cleanup(int threshold, Function? onCleanup) {\n    final memory = ProcessInfo.currentRss / 1024 / 1024;\n    logger.d('Memory cleanup, current memory: ${memory.toInt()}M, threshold: ${threshold}M');\n    if (memory > threshold) {\n      onCleanup?.call();\n      logger.i('Memory cleanup, current memory: ${memory.toInt()}M, threshold: ${threshold}M, cleanup');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/model/search_model.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'package:get/get.dart';\nimport 'package:proxypin/network/http/content_type.dart';\nimport 'package:proxypin/network/http/http.dart';\n\n/// @author wanghongen\n/// 2023/8/4\nclass SearchModel {\n  String? keyword;\n\n  //是否区分大小写\n  RxBool caseSensitive = RxBool(false);\n\n  //搜索范围\n  Set<Option> searchOptions = {Option.url};\n\n  //请求方法\n  HttpMethod? requestMethod;\n\n  //请求类型\n  ContentType? requestContentType;\n\n  //响应类型\n  ContentType? responseContentType;\n\n  // 状态码范围（包含两端）\n  int? statusCodeFrom;\n  int? statusCodeTo;\n\n  // 耗时范围，单位毫秒（包含两端）\n  int? durationFromMs;\n  int? durationToMs;\n\n  // 协议过滤，可选：HTTP (any), WS, HTTP1, H2. 如果为空则不过滤\n  Set<Protocol> protocols = {};\n\n  SearchModel([this.keyword]);\n\n  bool get isNotEmpty {\n    return keyword?.trim().isNotEmpty == true ||\n        requestMethod != null ||\n        requestContentType != null ||\n        responseContentType != null ||\n        statusCodeFrom != null ||\n        statusCodeTo != null ||\n        durationFromMs != null ||\n        durationToMs != null ||\n        protocols.isNotEmpty;\n  }\n\n  bool get isEmpty {\n    return !isNotEmpty;\n  }\n\n  ///复制对象\n  SearchModel clone() {\n    var searchModel = SearchModel(keyword);\n    searchModel.searchOptions = Set.from(searchOptions);\n    searchModel.requestMethod = requestMethod;\n    searchModel.requestContentType = requestContentType;\n    searchModel.responseContentType = responseContentType;\n    searchModel.statusCodeFrom = statusCodeFrom;\n    searchModel.statusCodeTo = statusCodeTo;\n    searchModel.durationFromMs = durationFromMs;\n    searchModel.durationToMs = durationToMs;\n    searchModel.protocols = Set.from(protocols);\n    searchModel.caseSensitive = RxBool(caseSensitive.value);\n    return searchModel;\n  }\n\n  @override\n  String toString() {\n    return 'SearchModel{keyword: $keyword, searchOptions: $searchOptions, responseContentType: $responseContentType, requestMethod: $requestMethod, requestContentType: $requestContentType, statusRange: [$statusCodeFrom-$statusCodeTo], durationRangeMs: [$durationFromMs-$durationToMs], protocols: $protocols}';\n  }\n\n  ///是否匹配\n  bool filter(HttpRequest request, HttpResponse? response) {\n    if (isEmpty) {\n      return true;\n    }\n\n    if (requestMethod != null && requestMethod != request.method) {\n      return false;\n    }\n    if (requestContentType != null && request.contentType != requestContentType) {\n      return false;\n    }\n\n    if (responseContentType != null && response?.contentType != responseContentType) {\n      return false;\n    }\n\n    // status range\n    if ((statusCodeFrom != null || statusCodeTo != null) && response != null) {\n      var code = response.status.code;\n      if (statusCodeFrom != null && code < statusCodeFrom!) {\n        return false;\n      }\n      if (statusCodeTo != null && code > statusCodeTo!) {\n        return false;\n      }\n    }\n\n    // duration range\n    if ((durationFromMs != null || durationToMs != null) && response != null) {\n      var cost = response.responseTime.difference(request.requestTime).inMilliseconds;\n      if (durationFromMs != null && cost < durationFromMs!) {\n        return false;\n      }\n      if (durationToMs != null && cost > durationToMs!) {\n        return false;\n      }\n    }\n\n    // protocol filters\n    if (protocols.isNotEmpty) {\n      bool matched = false;\n      for (var p in protocols) {\n        if (_matchProtocol(p, request, response)) {\n          matched = true;\n          break;\n        }\n      }\n      if (!matched) {\n        return false;\n      }\n    }\n\n    if (keyword == null || keyword?.isEmpty == true || searchOptions.isEmpty) {\n      return true;\n    }\n\n    for (var option in searchOptions) {\n      if (keywordFilter(keyword!, caseSensitive.value, option, request, response)) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  bool _matchProtocol(Protocol p, HttpRequest request, HttpResponse? response) {\n    switch (p) {\n      case Protocol.https:\n        return request.hostAndPort?.scheme == 'https://';\n      case Protocol.http:\n        return request.requestUrl.startsWith('http://');\n      case Protocol.ws:\n        return request.isWebSocket || (response != null && response.isWebSocket == true);\n      case Protocol.http1:\n        return request.protocolVersion == 'HTTP/1.1';\n      case Protocol.h2:\n        return request.protocolVersion == 'HTTP/2' || request.protocolVersion == 'h2';\n    }\n  }\n\n  ///关键字过滤\n  bool keywordFilter(String keyword, bool caseSensitive, Option option, HttpRequest request, HttpResponse? response) {\n    if (option == Option.url) {\n      if (caseSensitive) {\n        return request.requestUrl.contains(keyword);\n      }\n      return request.requestUrl.toLowerCase().contains(keyword.toLowerCase());\n    }\n\n    if (option == Option.method) {\n      return caseSensitive\n          ? request.method.name.contains(keyword)\n          : request.method.name.toLowerCase().contains(keyword.toLowerCase());\n    }\n    if (option == Option.responseContentType && response?.headers.contentType.contains(keyword) == true) {\n      return true;\n    }\n\n    if (option == Option.requestBody && request.bodyAsString.contains(keyword) == true) {\n      return true;\n    }\n    if (option == Option.responseBody && response?.bodyAsString.contains(keyword) == true) {\n      return true;\n    }\n\n    if (option == Option.requestHeader || option == Option.responseHeader) {\n      var entries = option == Option.requestHeader ? request.headers.entries : response?.headers.entries ?? [];\n\n      for (var entry in entries) {\n        if (caseSensitive) {\n          if (entry.key.contains(keyword) || entry.value.any((element) => element.contains(keyword))) {\n            return true;\n          }\n        } else {\n          if (entry.key.toLowerCase() == keyword.toLowerCase() ||\n              entry.value.any((element) => element.toLowerCase().contains(keyword.toLowerCase()))) {\n            return true;\n          }\n        }\n      }\n    }\n    return false;\n  }\n}\n\nenum Option {\n  url,\n  method,\n  responseContentType,\n  requestHeader,\n  requestBody,\n  responseHeader,\n  responseBody,\n}\n\n/// 协议快速筛选\nenum Protocol { http, https, ws, http1, h2 }\n"
  },
  {
    "path": "lib/ui/component/multi_window.dart",
    "content": "﻿/*\n * Copyright 2023 Hongen Wang\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/components/manager/request_crypto_manager.dart';\nimport 'package:proxypin/network/components/manager/request_breakpoint_manager.dart';\nimport 'package:proxypin/network/components/manager/request_map_manager.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/network/components/manager/script_manager.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/network/components/request_breakpoint.dart';\nimport 'package:proxypin/ui/component/device.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/content/body.dart';\nimport 'package:proxypin/ui/content/panel.dart';\nimport 'package:proxypin/ui/desktop/debug/breakpoint_executor.dart';\nimport 'package:proxypin/ui/desktop/request/request_editor.dart';\nimport 'package:proxypin/ui/desktop/setting/request_rewrite.dart';\nimport 'package:proxypin/ui/desktop/setting/script.dart';\nimport 'package:proxypin/ui/toolbox/aes_page.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:url_launcher/url_launcher.dart';\nimport 'package:window_manager/window_manager.dart';\n\nimport '../desktop/setting/request_breakpoint.dart';\nimport '../desktop/setting/request_crypto.dart';\nimport '../desktop/setting/request_map.dart';\nimport '../toolbox/cert_hash.dart';\nimport '../toolbox/encoder.dart';\nimport '../toolbox/js_run.dart';\nimport '../toolbox/qr_code_page.dart';\nimport '../toolbox/regexp.dart';\nimport '../toolbox/timestamp.dart';\nimport '../toolbox/websocket_request.dart';\n\nbool isMultiWindow = false;\n\n///多窗口\nWidget multiWindow(int windowId, Map<dynamic, dynamic> argument) {\n  isMultiWindow = true;\n  //请求编辑器\n  if (argument['name'] == 'RequestEditor') {\n    return RequestEditor(\n        windowController: WindowController.fromWindowId(windowId),\n        request: argument['request'] == null ? null : HttpRequest.fromJson(argument['request']));\n  }\n\n  //请求详情\n  if (argument['name'] == 'RequestDetailPage') {\n    return NetworkTabController(\n      windowId: windowId,\n      httpRequest: argument['request'] == null ? null : HttpRequest.fromJson(argument['request']),\n      httpResponse: argument['response'] == null ? null : HttpResponse.fromJson(argument['response']),\n    );\n  }\n\n  //请求体\n  if (argument['name'] == 'HttpBodyWidget') {\n    return HttpBodyWidget(\n        windowController: WindowController.fromWindowId(windowId),\n        httpMessage: HttpMessage.fromJson(argument['httpMessage']),\n        inNewWindow: true,\n        hideRequestRewrite: true);\n  }\n  //编码\n  if (argument['name'] == 'EncoderWidget') {\n    return EncoderWidget(\n        type: EncoderType.nameOf(argument['type']),\n        text: argument['text'],\n        windowController: WindowController.fromWindowId(windowId));\n  }\n  //脚本\n  if (argument['name'] == 'ScriptWidget') {\n    return ScriptWidget(windowId: windowId);\n  }\n  //请求重写\n  if (argument['name'] == 'RequestRewriteWidget') {\n    return futureWidget(\n        RequestRewriteManager.instance, (data) => RequestRewriteWidget(windowId: windowId, requestRewrites: data));\n  }\n\n  // 请求加密\n  if (argument['name'] == 'RequestCryptoPage') {\n    return futureWidget(RequestCryptoManager.instance, (data) => RequestCryptoPage(windowId: windowId, manager: data));\n  }\n  // 请求映射\n  if (argument['name'] == 'RequestMapPage') {\n    return RequestMapPage(windowId: windowId);\n  }\n\n  // 请求拦截\n  if (argument['name'] == 'RequestBreakpointPage') {\n    return futureWidget(\n        RequestBreakpointManager.instance, (manager) => RequestBreakpointPage(windowId: windowId, manager: manager));\n  }\n\n  if (argument['name'] == 'QrCodePage') {\n    return QrCodePage(windowId: windowId);\n  }\n\n  if (argument['name'] == 'CertHashPage') {\n    return CertHashPage(windowId: windowId);\n  }\n\n  if (argument['name'] == 'JavaScript') {\n    return JavaScript(windowId: windowId);\n  }\n\n  if (argument['name'] == 'RegExpPage') {\n    return RegExpPage(windowId: windowId);\n  }\n  if (argument['name'] == 'TimestampPage') {\n    return TimestampPage(windowId: windowId);\n  }\n\n  if (argument['name'] == 'AesPage') {\n    return AesPage();\n  }\n\n  //脚本日志\n  if (argument['name'] == 'ScriptConsoleWidget') {\n    return ScriptConsoleWidget(windowId: windowId);\n  }\n\n  if (argument['name'] == 'WebSocketRequestPage') {\n    return WebSocketRequestPage(windowId: windowId);\n  }\n\n  if (argument['name'] == 'BreakpointExecutor') {\n    return BreakpointExecutor(\n      windowId: windowId,\n      request: HttpRequest.fromJson(argument['request']),\n      response: argument['response'] == null ? null : HttpResponse.fromJson(argument['response']),\n      isResponse: argument['type'] == 'response',\n      requestId: argument['requestId'],\n    );\n  }\n\n  return const SizedBox();\n}\n\nenum Operation {\n  add,\n  update,\n  delete,\n  enabled,\n  refresh;\n\n  static Operation of(String name) {\n    return values.firstWhere((element) => element.name == name);\n  }\n}\n\nclass MultiWindow {\n  static Function(String widgetName, Map<String, dynamic>? args)? onOpenWindow;\n\n  /// 刷新请求重写\n  static Future<void> invokeRefreshRewrite(Operation operation,\n      {int? index, RequestRewriteRule? rule, List<RewriteItem>? items, bool? enabled}) async {\n    await DesktopMultiWindow.invokeMethod(0, \"refreshRequestRewrite\", {\n      \"enabled\": enabled,\n      \"operation\": operation.name,\n      'index': index,\n      'rule': rule?.toJson(),\n      'items': items?.map((e) => e.toJson()).toList()\n    });\n  }\n\n  static Future<WindowController> openWindow(String title, String widgetName,\n      {Size size = const Size(800, 680), Map<String, dynamic>? args}) async {\n    if (Platform.isAndroid || Platform.isIOS) {\n      onOpenWindow?.call(widgetName, args);\n      return WindowController.fromWindowId(0); // Dummy controller\n    }\n\n    var ratio = 1.0;\n    if (Platform.isWindows) {\n      ratio = WindowManager.instance.getDevicePixelRatio();\n    }\n    registerMethodHandler();\n    final window = await DesktopMultiWindow.createWindow(jsonEncode(\n      {'name': widgetName, ...?args},\n    ));\n    window.setTitle(title);\n    window\n      ..setFrame(const Offset(50, -10) & Size(size.width * ratio, size.height * ratio))\n      ..center();\n    window.show();\n\n    return window;\n  }\n\n  static bool _refreshRewrite = false;\n\n  static Future<void> _handleRefreshRewrite(Operation operation, Map<dynamic, dynamic> arguments) async {\n    RequestRewriteManager requestRewrites = await RequestRewriteManager.instance;\n\n    switch (operation) {\n      case Operation.add:\n      case Operation.update:\n        var rule = RequestRewriteRule.formJson(arguments['rule']);\n        List<dynamic>? list = arguments['items'] as List<dynamic>?;\n        List<RewriteItem>? items = list?.map((e) => RewriteItem.fromJson(e)).toList();\n\n        if (operation == Operation.add) {\n          await requestRewrites.addRule(rule, items!);\n        } else {\n          await requestRewrites.updateRule(arguments['index'], rule, items);\n        }\n        break;\n      case Operation.delete:\n        var rule = requestRewrites.rules.removeAt(arguments['index']);\n        requestRewrites.rewriteItemsCache.remove(rule); //删除缓存\n        break;\n      case Operation.enabled:\n        requestRewrites.enabled = arguments['enabled'];\n        break;\n      default:\n        break;\n    }\n\n    if (_refreshRewrite) return;\n    _refreshRewrite = true;\n    Future.delayed(const Duration(milliseconds: 1000), () async {\n      _refreshRewrite = false;\n      requestRewrites.flushRequestRewriteConfig();\n    });\n  }\n}\n\nbool _registerHandler = false;\n\n/// 桌面端多窗口 注册方法处理器\nvoid registerMethodHandler() {\n  if (_registerHandler) {\n    return;\n  }\n  _registerHandler = true;\n  DesktopMultiWindow.setMethodHandler((call, fromWindowId) async {\n    logger.d('${call.method} $fromWindowId');\n\n    if (call.method == 'getProxyInfo') {\n      return ProxyServer.current?.isRunning == true ? {'host': '127.0.0.1', 'port': ProxyServer.current!.port} : null;\n    }\n    if (call.method == 'refreshRequestRewrite') {\n      await MultiWindow._handleRefreshRewrite(Operation.of(call.arguments['operation']), call.arguments);\n      return 'done';\n    }\n\n    if (call.method == 'refreshScript') {\n      await ScriptManager.instance.then((value) {\n        return value.reloadScript();\n      });\n      return 'done';\n    }\n\n    if (call.method == 'refreshRequestMap') {\n      await RequestMapManager.instance.then((value) {\n        return value.reloadConfig();\n      });\n      return 'done';\n    }\n\n    if (call.method == 'refreshRequestCrypto') {\n      await RequestCryptoManager.instance.then((value) {\n        return value.reloadConfig();\n      });\n      return 'done';\n    }\n\n    if (call.method == 'refreshRequestBreakpoint') {\n      await RequestBreakpointManager.instance.then((value) {\n        return value.load();\n      });\n      return 'done';\n    }\n\n    if (call.method == 'pickFiles') {\n      var extensions = call.arguments != null ? call.arguments['allowedExtensions'] : null;\n      FilePickerResult? result = await FilePicker.platform.pickFiles(\n          type: extensions == null ? FileType.any : FileType.custom,\n          allowedExtensions: extensions == null ? null : List.from(extensions),\n          initialDirectory: \"/Downloads\");\n      if (result == null || result.files.isEmpty) return null;\n      return result.files.single.path;\n    }\n\n    if (call.method == 'saveFile') {\n      return await FilePicker.platform.saveFile(fileName: call.arguments['fileName']);\n    }\n\n    if (call.method == 'getApplicationSupportDirectory') {\n      return getApplicationSupportDirectory().then((it) => it.path);\n    }\n\n    if (call.method == 'launchUrl') {\n      return launchUrl(Uri.parse(call.arguments));\n    }\n\n    if (call.method == 'registerConsoleLog') {\n      ScriptManager.registerConsoleLog(fromWindowId);\n      return \"done\";\n    }\n\n    if (call.method == 'deviceId') {\n      return await DeviceUtils.desktopDeviceId();\n    }\n\n    if (call.method == 'resumeRequest') {\n      var request = call.arguments['request'] == null\n          ? null\n          : HttpRequest.fromJson(jsonDecode(jsonEncode(call.arguments['request'])));\n      RequestBreakpointInterceptor.instance.resumeRequest(call.arguments['requestId'], request);\n      return 'done';\n    }\n\n    if (call.method == 'resumeResponse') {\n      var response = call.arguments['response'] == null\n          ? null\n          : HttpResponse.fromJson(jsonDecode(jsonEncode(call.arguments['response'])));\n      if (response != null) {\n        response.requestId = call.arguments['requestId'];\n      }\n      RequestBreakpointInterceptor.instance.resumeResponse(call.arguments['requestId'], response);\n      return 'done';\n    }\n\n    return 'done';\n  });\n}\n\n///打开编码窗口\nFuture<void> encodeWindow(EncoderType type, BuildContext context, [String? text]) async {\n  if (Platforms.isMobile()) {\n    Navigator.of(context).push(MaterialPageRoute(builder: (context) => EncoderWidget(type: type, text: text)));\n    return;\n  }\n\n  var ratio = 1.0;\n  if (Platform.isWindows) {\n    ratio = WindowManager.instance.getDevicePixelRatio();\n  }\n  final window = await DesktopMultiWindow.createWindow(jsonEncode(\n    {'name': 'EncoderWidget', 'type': type.name, 'text': text},\n  ));\n  if (!context.mounted) return;\n  window.setTitle(AppLocalizations.of(context)!.encode);\n  window\n    ..setFrame(const Offset(80, 80) & Size(900 * ratio, 600 * ratio))\n    ..center()\n    ..show();\n}\n\nFuture<void> openScriptConsoleWindow() async {\n  var ratio = 1.0;\n  if (Platform.isWindows) {\n    ratio = WindowManager.instance.getDevicePixelRatio();\n  }\n  final window = await DesktopMultiWindow.createWindow(jsonEncode(\n    {'name': 'ScriptConsoleWidget'},\n  ));\n  window.setTitle('Script Console');\n  window\n    ..setFrame(const Offset(50, 0) & Size(900 * ratio, 650 * ratio))\n    ..center();\n  window.show();\n}\n"
  },
  {
    "path": "lib/ui/component/proxy_port_setting.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/server.dart';\n\nclass PortWidget extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final TextStyle? textStyle;\n  final String? title;\n\n  const PortWidget({super.key, required this.proxyServer, this.textStyle, this.title});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _PortState();\n  }\n}\n\nclass _PortState extends State<PortWidget> {\n  final textController = TextEditingController();\n  final FocusNode portFocus = FocusNode();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    textController.text = widget.proxyServer.port.toString();\n    portFocus.addListener(() async {\n      //失去焦点\n      if (!portFocus.hasFocus && textController.text != widget.proxyServer.port.toString()) {\n        final port = int.tryParse(textController.text) ?? -1;\n        if (port < 0 || port > 65535) {\n          textController.text = widget.proxyServer.port.toString();\n          FlutterToastr.show(\"Port out of range 0-65535\", context, duration: 3);\n          return;\n        }\n\n        widget.proxyServer.configuration.port = port;\n\n        if (widget.proxyServer.isRunning) {\n          String message = localizations.proxyPortRepeat(widget.proxyServer.port);\n          widget.proxyServer.restart().catchError((e) => FlutterToastr.show(message, context, duration: 3));\n        }\n        widget.proxyServer.configuration.flushConfig();\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    portFocus.dispose();\n    textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(children: [\n      const Padding(padding: EdgeInsets.only(left: 15)),\n      Text(widget.title ?? localizations.port, style: widget.textStyle),\n      SizedBox(\n          width: 80,\n          child: TextFormField(\n            focusNode: portFocus,\n            controller: textController,\n            textAlign: TextAlign.center,\n            onTapOutside: (event) => portFocus.unfocus(),\n            keyboardType: TextInputType.datetime,\n            inputFormatters: <TextInputFormatter>[\n              LengthLimitingTextInputFormatter(5),\n              FilteringTextInputFormatter.allow(RegExp(\"[0-9]\"))\n            ],\n            decoration: const InputDecoration(),\n          ))\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/qrcode/qr_scan_view.dart",
    "content": "import 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_qr_reader_plus/flutter_qr_reader.dart';\nimport 'package:permission_handler/permission_handler.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\n///@Author: Hongen Wang\n/// qr code scanner\nclass QrCodeScanner {\n  static Future<String?> scan(BuildContext context) async {\n    var status = await Permission.camera.status;\n\n    if (!status.isGranted) {\n      status = await Permission.camera.request();\n    }\n\n    if (!status.isGranted) {\n      if (!context.mounted) return Future.value(null);\n      AppLocalizations localizations = AppLocalizations.of(context)!;\n      bool isCN = localizations.localeName == 'zh';\n      await showDialog(\n          context: context,\n          builder: (context) => AlertDialog(\n                content: Text(isCN ? \"请授予相机权限\" : \"Please grant camera permission\"),\n                actions: <Widget>[\n                  TextButton(\n                    onPressed: () => Navigator.of(context).pop(),\n                    child: Text(localizations.cancel),\n                  ),\n                  TextButton(\n                    onPressed: () async {\n                      if (!context.mounted) return Future.value(null);\n                      Navigator.of(context).pop();\n                      final PermissionStatus newStatus = await Permission.camera.request();\n                      // Flutter权限处理有bug  url: https://github.com/Baseflow/flutter-permission-handler/issues/1206\n                      if (newStatus.isRestricted || newStatus.isPermanentlyDenied) {\n                        openAppSettings();\n                      }\n                    },\n                    child: Text(localizations.confirm),\n                  ),\n                ],\n              ));\n      return Future.value(null);\n    }\n\n    if (!context.mounted) return Future.value(null);\n\n    return await Navigator.of(context, rootNavigator: true)\n        .push<String>(MaterialPageRoute(builder: (context) => QeCodeScanView()));\n  }\n}\n\nclass QeCodeScanView extends StatefulWidget {\n  const QeCodeScanView({super.key});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _QrReaderViewState();\n  }\n}\n\nclass _QrReaderViewState extends State<QeCodeScanView> with TickerProviderStateMixin {\n  final int animationTime = 2000;\n  QrReaderViewController? _controller;\n  AnimationController? _animationController;\n\n  bool isScan = false;\n  bool openFlashlight = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void dispose() {\n    stop();\n    super.dispose();\n  }\n\n  void _onCreateController(QrReaderViewController controller) async {\n    _controller = controller;\n    startScan();\n  }\n\n  void startScan() async {\n    isScan = true;\n\n    _controller?.startCamera((data, _) async {\n      logger.d(\"scan qrCode data handle: $data\");\n      await handle(data);\n    });\n\n    _initAnimation();\n  }\n\n  handle(String data) async {\n    if (!isScan) return;\n    stop();\n    if (mounted) await Navigator.of(context, rootNavigator: true).maybePop(data);\n  }\n\n  void _initAnimation() {\n    _animationController ??= AnimationController(vsync: this, duration: Duration(milliseconds: animationTime));\n    _animationController\n      ?..addListener(_upState)\n      ..addStatusListener((state) {\n        if (!mounted) {\n          stop();\n          return;\n        }\n\n        if (state == AnimationStatus.completed) {\n          Future.delayed(Duration(seconds: 1), () {\n            _animationController?.reverse();\n          });\n        } else if (state == AnimationStatus.dismissed) {\n          Future.delayed(Duration(seconds: 1), () {\n            _animationController?.forward();\n          });\n        }\n      });\n\n    _animationController?.forward();\n  }\n\n  void stop() {\n    if (!isScan) {\n      return;\n    }\n\n    isScan = false;\n    _controller?.stopCamera();\n    _controller = null;\n    if (_animationController != null) {\n      _animationController?.stop();\n      _animationController?.dispose();\n      _animationController = null;\n    }\n  }\n\n  void _upState() {\n    setState(() {});\n  }\n\n  setFlashlight() async {\n    if (!isScan) return false;\n    _controller?.setFlashlight();\n    setState(() {\n      openFlashlight = !openFlashlight;\n    });\n  }\n\n  scanImage(String path) {\n    FlutterQrReader.imgScan(path).then((value) {\n      stop();\n      if (mounted) {\n        Navigator.of(context, rootNavigator: true).pop(value ?? \"-1\");\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n        color: Colors.black,\n        child: LayoutBuilder(builder: (context, constraints) {\n          final qrScanSize = constraints.maxWidth * 0.85;\n          final mediaQuery = MediaQuery.of(context);\n\n          return Stack(\n            children: <Widget>[\n              SizedBox(\n                  width: constraints.maxWidth,\n                  height: constraints.maxHeight,\n                  child: QrReaderView(\n                    width: constraints.maxWidth,\n                    height: constraints.maxHeight,\n                    autoFocusIntervalInMs: 1000,\n                    callback: _onCreateController,\n                  )),\n              Positioned(\n                left: (constraints.maxWidth - qrScanSize) / 2,\n                top: (constraints.maxHeight - qrScanSize) * 0.333333,\n                child: CustomPaint(\n                  painter: QrScanBoxPainter(\n                    boxLineColor: Theme.of(context).colorScheme.primary,\n                    animationValue: _animationController?.value ?? 0,\n                    isForward: _animationController?.status == AnimationStatus.forward,\n                  ),\n                  child: SizedBox(width: qrScanSize, height: qrScanSize),\n                ),\n              ),\n              Positioned(\n                width: constraints.maxWidth,\n                bottom: constraints.maxHeight == mediaQuery.size.height ? 12 + mediaQuery.padding.top : 12,\n                child: Row(\n                  crossAxisAlignment: CrossAxisAlignment.center,\n                  mainAxisAlignment: MainAxisAlignment.spaceAround,\n                  children: <Widget>[\n                    IconButton(\n                      onPressed: () async {\n                        final result = await FilePicker.platform.pickFiles(\n                          type: FileType.image,\n                          allowMultiple: false,\n                        );\n                        if (result == null || result.files.isEmpty) return;\n                        final path = result.files.first.path;\n                        if (path == null) return;\n                        scanImage(path);\n                      },\n                      icon: Icon(Icons.photo_library, color: Colors.white, size: 35),\n                    ),\n                    IconButton(\n                      onPressed: setFlashlight,\n                      icon: Icon(openFlashlight ? Icons.flash_on : Icons.flash_off, size: 35, color: Colors.white),\n                    ),\n                    TextButton(\n                        onPressed: () {\n                          stop();\n                          Navigator.of(context, rootNavigator: true).pop();\n                        },\n                        child: Text(localizations.cancel, style: TextStyle(color: Colors.white, fontSize: 18))),\n                  ],\n                ),\n              )\n            ],\n          );\n        }));\n  }\n}\n\nclass QrScanBoxPainter extends CustomPainter {\n  final double animationValue;\n  final bool isForward;\n  final Color boxLineColor;\n\n  QrScanBoxPainter({required this.animationValue, required this.isForward, required this.boxLineColor});\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    final borderRadius = BorderRadius.all(Radius.circular(12)).toRRect(\n      Rect.fromLTWH(0, 0, size.width, size.height),\n    );\n    canvas.drawRRect(\n      borderRadius,\n      Paint()\n        ..color = Colors.white54\n        ..style = PaintingStyle.stroke\n        ..strokeWidth = 1,\n    );\n    final borderPaint = Paint()\n      ..color = Colors.white\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 2;\n    final path = Path();\n    // leftTop\n    path.moveTo(0, 50);\n    path.lineTo(0, 12);\n    path.quadraticBezierTo(0, 0, 12, 0);\n    path.lineTo(50, 0);\n    // rightTop\n    path.moveTo(size.width - 50, 0);\n    path.lineTo(size.width - 12, 0);\n    path.quadraticBezierTo(size.width, 0, size.width, 12);\n    path.lineTo(size.width, 50);\n    // rightBottom\n    path.moveTo(size.width, size.height - 50);\n    path.lineTo(size.width, size.height - 12);\n    path.quadraticBezierTo(size.width, size.height, size.width - 12, size.height);\n    path.lineTo(size.width - 50, size.height);\n    // leftBottom\n    path.moveTo(50, size.height);\n    path.lineTo(12, size.height);\n    path.quadraticBezierTo(0, size.height, 0, size.height - 12);\n    path.lineTo(0, size.height - 50);\n\n    canvas.drawPath(path, borderPaint);\n\n    canvas.clipRRect(BorderRadius.all(Radius.circular(12)).toRRect(Offset.zero & size));\n\n    // Draw a single horizontal line\n    final linePaint = Paint()\n      ..color = boxLineColor\n      ..strokeWidth = 2.0;\n    final lineY = size.height * animationValue;\n    canvas.drawLine(\n      Offset(0, lineY),\n      Offset(size.width, lineY),\n      linePaint,\n    );\n  }\n\n  @override\n  bool shouldRepaint(QrScanBoxPainter oldDelegate) => animationValue != oldDelegate.animationValue;\n\n  @override\n  bool shouldRebuildSemantics(QrScanBoxPainter oldDelegate) => animationValue != oldDelegate.animationValue;\n}\n"
  },
  {
    "path": "lib/ui/component/search/highlight_text.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:proxypin/ui/component/search/search_controller.dart';\n\nclass HighlightTextWidget extends StatelessWidget {\n  final String text;\n  final TextStyle? style;\n  final EditableTextContextMenuBuilder? contextMenuBuilder;\n  final SearchTextController searchController;\n\n  const HighlightTextWidget(\n      {super.key, required this.text, this.contextMenuBuilder, required this.searchController, this.style});\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedBuilder(\n      animation: searchController,\n      builder: (context, child) {\n        final spans = _highlightMatches(context);\n        return SelectableText.rich(\n          TextSpan(children: spans),\n          showCursor: true,\n          contextMenuBuilder: contextMenuBuilder,\n        );\n      },\n    );\n  }\n\n  List<InlineSpan> _highlightMatches(BuildContext context) {\n    if (!searchController.shouldSearch()) {\n      return [TextSpan(text: text, style: style)];\n    }\n\n    final pattern = searchController.value.pattern;\n    final regex = searchController.value.isRegExp\n        ? RegExp(pattern, caseSensitive: searchController.value.isCaseSensitive)\n        : RegExp(\n            RegExp.escape(pattern),\n            caseSensitive: searchController.value.isCaseSensitive,\n          );\n\n    final spans = <InlineSpan>[];\n    int start = 0;\n    var allMatches = regex.allMatches(text).toList();\n    final currentIndex = searchController.currentMatchIndex.value;\n    ColorScheme colorScheme = ColorScheme.of(context);\n    List<GlobalKey> matchKeys = [];\n    for (int i = 0; i < allMatches.length; i++) {\n      final match = allMatches[i];\n      if (match.start > start) {\n        spans.add(TextSpan(text: text.substring(start, match.start), style: style));\n      }\n\n      // 为每个高亮项分配一个 GlobalKey\n      final key = GlobalKey();\n      matchKeys.add(key);\n      spans.add(WidgetSpan(\n          alignment: PlaceholderAlignment.middle,\n          baseline: TextBaseline.ideographic,\n          child: Container(\n            key: key,\n            color: i == currentIndex ? colorScheme.primary : colorScheme.inversePrimary,\n            child: Text(text.substring(match.start, match.end), style: style),\n          )));\n      start = match.end;\n    }\n    if (start < text.length) {\n      spans.add(TextSpan(text: text.substring(start), style: style));\n    }\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      searchController.updateMatchCount(allMatches.length);\n      _scrollToMatch(context, matchKeys);\n      matchKeys.clear();\n    });\n\n    return spans;\n  }\n\n  void _scrollToMatch(BuildContext context, List<GlobalKey> matchKeys) {\n    if (matchKeys.isNotEmpty) {\n      final currentIndex = searchController.currentMatchIndex.value;\n      if (currentIndex >= 0 && currentIndex < matchKeys.length) {\n        final key = matchKeys[currentIndex];\n        final context = key.currentContext;\n        if (context != null) {\n          Scrollable.ensureVisible(\n            context,\n            duration: const Duration(milliseconds: 300),\n            alignment: 0.5, // 高亮项在视图中的位置\n          );\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/search/search_controller.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get_rx/src/rx_types/rx_types.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/search/search_field.dart';\n\nclass SearchTextController extends ValueNotifier<SearchSettings> with WidgetsBindingObserver {\n  SearchTextController() : super(SearchSettings.empty) {\n    patternController.addListener(_onPatternControllerChanged);\n    WidgetsBinding.instance.addObserver(this); // 添加监听器\n  }\n\n  final patternController = TextEditingController();\n  RxInt currentMatchIndex = RxInt(0);\n  RxInt totalMatchCount = RxInt(0);\n\n  OverlayEntry? _searchPopup;\n  double? overlayTop;\n  double? overlayRight;\n\n  bool shouldSearch() {\n    return isSearchOverlayVisible && patternController.text.isNotEmpty;\n  }\n\n  void toggleCaseSensitivity() {\n    value = value.copyWith(isCaseSensitive: !value.isCaseSensitive);\n  }\n\n  void toggleIsRegExp() {\n    value = value.copyWith(isRegExp: !value.isRegExp);\n  }\n\n  void _onPatternControllerChanged() {\n    value = value.copyWith(pattern: patternController.text, currentMatchIndex: 0);\n    if (value.pattern.isEmpty) {\n      currentMatchIndex.value = 0;\n      totalMatchCount.value = 0;\n    }\n  }\n\n  void updateMatchCount(int count) {\n    totalMatchCount.value = count;\n    if (currentMatchIndex.value > count) {\n      currentMatchIndex.value = count;\n    }\n  }\n\n  void movePrevious() {\n    if (totalMatchCount.value == 0) return;\n    if (currentMatchIndex.value == 0) {\n      currentMatchIndex.value = totalMatchCount.value - 1;\n    } else {\n      currentMatchIndex.value--;\n    }\n    value = value.copyWith(currentMatchIndex: currentMatchIndex.value);\n  }\n\n  void moveNext() {\n    if (totalMatchCount.value == 0) return;\n    if (currentMatchIndex.value >= totalMatchCount.value - 1) {\n      currentMatchIndex.value = 0;\n    } else {\n      currentMatchIndex.value++;\n    }\n    value = value.copyWith(currentMatchIndex: currentMatchIndex.value);\n  }\n\n  void closeSearch() {\n    removeSearchOverlay();\n  }\n\n  void updateOverlayPosition(double top, double right) {\n    overlayTop = top;\n    overlayRight = right;\n    if (_searchPopup != null) {\n      _searchPopup!.markNeedsBuild();\n    }\n  }\n\n  @override\n  void didChangeMetrics() {\n    super.didChangeMetrics();\n    if (!isSearchOverlayVisible) {\n      return;\n    }\n\n    // 检测键盘弹出并调整位置\n    var view = WidgetsBinding.instance.platformDispatcher.views.first;\n    final bottomInset = MediaQueryData.fromView(view).viewInsets.bottom;\n    if (bottomInset == 0 || overlayTop == null) {\n      // 键盘收起\n      return;\n    }\n\n    var screenHeight = MediaQueryData.fromView(view).size.height;\n    final currentHeight = screenHeight - bottomInset;\n    if (overlayTop! + 50 > currentHeight) {\n      // 如果被键盘遮挡\n      updateOverlayPosition(max(currentHeight - 120, 120), overlayRight!); // 移动到键��上方\n    }\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this); // 移除监听器\n    logger.d('Disposing SearchTextController');\n    super.didChangeMetrics();\n    removeSearchOverlay();\n    patternController.dispose();\n    totalMatchCount.close();\n    currentMatchIndex.close();\n    super.dispose();\n  }\n\n  bool get isSearchOverlayVisible => _searchPopup != null;\n\n  void showSearchOverlay(BuildContext context, {double? top, double? right}) {\n    if (_searchPopup != null) {\n      return;\n    }\n\n    _searchPopup = _buildSearchOverlay(context, top: top, right: right);\n    Overlay.of(context).insert(_searchPopup!);\n  }\n\n  void removeSearchOverlay() {\n    _searchPopup?.remove();\n    _searchPopup = null;\n  }\n\n  OverlayEntry _buildSearchOverlay(BuildContext context, {double? top, double? right}) {\n    overlayTop = top ?? overlayTop;\n    overlayRight = right ?? overlayRight;\n    return OverlayEntry(\n      builder: (context) {\n        return Positioned(\n          top: overlayTop,\n          right: overlayRight,\n          child: Actions(actions: {\n            DismissIntent: CallbackAction<DismissIntent>(onInvoke: (intent) {\n              closeSearch();\n              return null;\n            }),\n          }, child: SearchField(searchController: this)),\n        );\n      },\n    );\n  }\n}\n\nclass SearchSettings {\n  const SearchSettings({\n    required this.isCaseSensitive,\n    required this.isRegExp,\n    required this.pattern,\n    this.currentMatchIndex = 0,\n  });\n\n  final bool isCaseSensitive;\n  final bool isRegExp;\n  final String pattern;\n  final int currentMatchIndex;\n\n  static const empty = SearchSettings(\n    isCaseSensitive: false,\n    isRegExp: false,\n    pattern: '',\n  );\n\n  SearchSettings copyWith({\n    bool? isCaseSensitive,\n    bool? isRegExp,\n    String? pattern,\n    int? currentMatchIndex,\n  }) {\n    return SearchSettings(\n      isCaseSensitive: isCaseSensitive ?? this.isCaseSensitive,\n      isRegExp: isRegExp ?? this.isRegExp,\n      pattern: pattern ?? this.pattern,\n      currentMatchIndex: currentMatchIndex ?? this.currentMatchIndex,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is SearchSettings &&\n          runtimeType == other.runtimeType &&\n          isCaseSensitive == other.isCaseSensitive &&\n          isRegExp == other.isRegExp &&\n          pattern == other.pattern &&\n          currentMatchIndex == other.currentMatchIndex;\n\n  @override\n  int get hashCode => isCaseSensitive.hashCode ^ isRegExp.hashCode ^ pattern.hashCode ^ currentMatchIndex.hashCode;\n}\n"
  },
  {
    "path": "lib/ui/component/search/search_field.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:proxypin/ui/component/search/search_controller.dart';\n\nimport '../../../utils/platform.dart';\n\nconst _hintText = 'Search…';\n\nclass SearchField extends StatefulWidget {\n  final SearchTextController searchController;\n\n  const SearchField({\n    super.key,\n    required this.searchController,\n  });\n\n  @override\n  State<SearchField> createState() => _SearchFieldState();\n}\n\nclass _SearchFieldState extends State<SearchField> {\n  final FocusNode focusNode = FocusNode();\n  final RxBool caseSensitive = RxBool(false);\n  final RxBool isRegExp = RxBool(false);\n\n  @override\n  initState() {\n    super.initState();\n    if (Platforms.isDesktop()) {\n      focusNode.requestFocus();\n    }\n    caseSensitive.value = widget.searchController.value.isCaseSensitive;\n    isRegExp.value = widget.searchController.value.isRegExp;\n  }\n\n  @override\n  dispose() {\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    double searchBoxWidth = min(450, MediaQuery.of(context).size.width - 20);\n\n    final searchBox = SizedBox(\n      width: searchBoxWidth,\n      child: Material(\n          elevation: 1,\n          child: Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, children: [\n            SizedBox(\n              width: Platforms.isDesktop() ? 260 : 220,\n              child: TextField(\n                autofocus: true,\n                focusNode: focusNode,\n                controller: widget.searchController.patternController,\n                onEditingComplete: () {\n                  widget.searchController.moveNext();\n                },\n                decoration: InputDecoration(\n                    hintText: _hintText,\n                    border: OutlineInputBorder(),\n                    isDense: true,\n                    contentPadding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),\n                    suffixIcon: Obx(() {\n                      return ToggleButtons(\n                        constraints: const BoxConstraints(minWidth: 30, minHeight: 43),\n                        onPressed: (index) {\n                          switch (index) {\n                            case 0:\n                              widget.searchController.toggleCaseSensitivity();\n                              caseSensitive.value = !caseSensitive.value;\n                              break;\n                            case 1:\n                              widget.searchController.toggleIsRegExp();\n                              isRegExp.value = !isRegExp.value;\n                              break;\n                          }\n                        },\n                        isSelected: [\n                          caseSensitive.value,\n                          isRegExp.value,\n                        ],\n                        children: const [\n                          Text('Aa'),\n                          Text('.*'),\n                        ],\n                      );\n                    })),\n              ),\n            ),\n            if (Platforms.isDesktop()) Obx(() => SizedBox(width: 85, child: _getText())),\n            if (Platforms.isMobile()) SizedBox(width: 10),\n            InkWell(\n              onTap: widget.searchController.movePrevious,\n              child: const Icon(Icons.north, size: 17),\n            ),\n            SizedBox(width: 10),\n            InkWell(\n              onTap: widget.searchController.moveNext,\n              child: const Icon(Icons.south, size: 17),\n            ),\n            const SizedBox(width: 3),\n            IconButton(\n              iconSize: 19,\n              icon: const Icon(Icons.close),\n              onPressed: () => widget.searchController.closeSearch(),\n            ),\n            const SizedBox(width: 10),\n          ])),\n    );\n\n    return Draggable<Offset>(\n      feedback: searchBox,\n      childWhenDragging: SizedBox(width: searchBoxWidth, height: 56),\n      onDragEnd: (details) {\n        final offset = details.offset;\n        final screenSize = MediaQuery.of(context).size;\n        double newTop = offset.dy;\n        double newRight = screenSize.width - offset.dx - searchBoxWidth;\n        widget.searchController.updateOverlayPosition(newTop, newRight);\n      },\n      child: searchBox,\n    );\n  }\n\n  Text _getText() {\n    if (widget.searchController.totalMatchCount.value == 0) {\n      return Text(\"0 results\",\n          textAlign: TextAlign.center,\n          style: TextStyle(color: widget.searchController.patternController.text.isNotEmpty ? Colors.red : null));\n    }\n\n    final currentMatchIndex = widget.searchController.currentMatchIndex.value + 1;\n    final totalMatchCount = widget.searchController.totalMatchCount.value;\n\n    return Text(\n      '$currentMatchIndex / $totalMatchCount',\n      textAlign: TextAlign.center,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/search_condition.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:get/get.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/http/content_type.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nimport 'model/search_model.dart';\n\n/// @author wanghongen\n/// 2023/8/6\nclass SearchConditions extends StatefulWidget {\n  final SearchModel searchModel;\n  final Function(SearchModel searchModel)? onSearch;\n  final EdgeInsetsGeometry? padding;\n\n  const SearchConditions({super.key, required this.searchModel, this.onSearch, this.padding});\n\n  @override\n  State<StatefulWidget> createState() {\n    return SearchConditionsState();\n  }\n}\n\nclass SearchConditionsState extends State<SearchConditions> {\n  final Map<String, ContentType?> requestContentMap = {\n    'JSON': ContentType.json,\n    'FORM-URL': ContentType.formUrl,\n    'FORM-DATA': ContentType.formData,\n  };\n\n  final Map<String, ContentType?> responseContentMap = {\n    'JSON': ContentType.json,\n    'HTML': ContentType.html,\n    'JS': ContentType.js,\n    'CSS': ContentType.css,\n    'TEXT': ContentType.text,\n    'IMAGE': ContentType.image\n  };\n\n  late SearchModel searchModel;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    searchModel = widget.searchModel.clone();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    requestContentMap[localizations.all] = null;\n    responseContentMap[localizations.all] = null;\n    Color primaryColor = ColorScheme.of(context).primary;\n    return Container(\n      padding: widget.padding,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: <Widget>[\n          // keyword\n          TextFormField(\n            initialValue: searchModel.keyword,\n            onChanged: (val) => searchModel.keyword = val,\n            onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n            decoration: InputDecoration(\n              isCollapsed: true,\n              contentPadding: const EdgeInsets.all(10),\n              border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(15))),\n              hintText: localizations.keyword,\n              suffixIcon: Obx(() => IconButton(\n                    tooltip: \"Case Sensitive\",\n                    icon: Text('Aa',\n                        style: TextStyle(\n                            fontWeight: FontWeight.w500, color: searchModel.caseSensitive.value ? primaryColor : null)),\n                    onPressed: () {\n                      searchModel.caseSensitive.value = !searchModel.caseSensitive.value;\n                    },\n                  )),\n            ),\n          ),\n          const SizedBox(height: 10),\n          // protocol quick selectors placed under the keyword input (very compact)\n          protocolsWidget(),\n          const SizedBox(height: 10),\n          // keyword scope\n          Text(localizations.keywordSearchScope),\n          const SizedBox(height: 10),\n          Wrap(\n            children: [\n              options('URL', Option.url),\n              options(localizations.requestHeader, Option.requestHeader),\n              options(localizations.requestBody, Option.requestBody),\n              options(localizations.responseHeader, Option.responseHeader),\n              options(localizations.responseBody, Option.responseBody),\n            ],\n          ),\n          const SizedBox(height: 10),\n\n          // request method\n          row(\n            Text('${localizations.requestMethod}:'),\n            DropdownMenu(\n              initialValue: searchModel.requestMethod?.name ?? localizations.all,\n              items: HttpMethod.methods().map((e) => e.name).toList()..insert(0, localizations.all),\n              onSelected: (String value) {\n                searchModel.requestMethod = value == localizations.all ? null : HttpMethod.valueOf(value);\n              },\n            ),\n          ),\n          const SizedBox(height: 10),\n          // request type\n          row(\n            Text('${localizations.requestType}:'),\n            DropdownMenu(\n              initialValue: Maps.getKey(requestContentMap, searchModel.requestContentType) ?? localizations.all,\n              items: requestContentMap.keys,\n              onSelected: (String value) {\n                searchModel.requestContentType = requestContentMap[value];\n              },\n            ),\n          ),\n          const SizedBox(height: 10),\n\n          // response type\n          row(\n            Text('${localizations.responseType}:'),\n            DropdownMenu(\n              initialValue: Maps.getKey(responseContentMap, searchModel.responseContentType) ?? localizations.all,\n              items: responseContentMap.keys,\n              onSelected: (String value) {\n                searchModel.responseContentType = responseContentMap[value];\n              },\n            ),\n          ),\n          const SizedBox(height: 10),\n\n          // status code range\n          row(\n            Text('${localizations.statusCode}: '),\n            Row(children: [\n              SizedBox(\n                  width: 55,\n                  height: 32,\n                  child: textField(\n                      initialValue: searchModel.statusCodeFrom?.toString(),\n                      onChanged: (val) => searchModel.statusCodeFrom = int.tryParse(val))),\n              const Padding(padding: EdgeInsets.symmetric(horizontal: 5), child: Text(\" - \")),\n              SizedBox(\n                  width: 55,\n                  height: 32,\n                  child: textField(\n                      initialValue: searchModel.statusCodeTo?.toString(),\n                      onChanged: (val) => searchModel.statusCodeTo = int.tryParse(val))),\n            ]),\n          ),\n          const SizedBox(height: 10),\n\n          // duration range (ms)\n          row(\n            Text('${localizations.duration} (ms): '),\n            Row(children: [\n              SizedBox(\n                  width: 55,\n                  height: 32,\n                  child: textField(\n                      initialValue: searchModel.durationFromMs?.toString(),\n                      onChanged: (val) => searchModel.durationFromMs = int.tryParse(val))),\n              const Padding(padding: EdgeInsets.symmetric(horizontal: 5), child: Text(\" - \")),\n              SizedBox(\n                  width: 55,\n                  height: 32,\n                  child: textField(\n                      initialValue: searchModel.durationToMs?.toString(),\n                      onChanged: (val) => searchModel.durationToMs = int.tryParse(val))),\n            ]),\n          ),\n          const SizedBox(height: 15),\n\n          // action buttons\n          Row(\n            mainAxisAlignment: MainAxisAlignment.end,\n            children: <Widget>[\n              TextButton(\n                onPressed: () => Navigator.pop(context),\n                child: Text(localizations.cancel, style: const TextStyle(fontSize: 14)),\n              ),\n              TextButton(\n                onPressed: () {\n                  widget.onSearch?.call(SearchModel());\n                  Navigator.pop(context);\n                },\n                child: Text(localizations.clearSearch, style: const TextStyle(fontSize: 14)),\n              ),\n              TextButton(\n                onPressed: () {\n                  widget.onSearch?.call(searchModel);\n                  Navigator.pop(context);\n                },\n                child: Text(localizations.confirm, style: const TextStyle(fontSize: 14)),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget protocolsWidget() {\n    Color primaryColor = ColorScheme.of(context).primary;\n    return Wrap(\n      spacing: 5,\n      runSpacing: 2,\n      children: <Widget>[\n        FilterChip(\n          label: const Text('HTTP'),\n          selected: searchModel.protocols.contains(Protocol.http),\n          showCheckmark: false,\n          selectedColor: primaryColor.withValues(alpha: 0.12),\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),\n          labelStyle: const TextStyle(fontSize: 12),\n          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n          visualDensity: const VisualDensity(horizontal: -3, vertical: -3),\n          onSelected: (sel) => setState(() {\n            sel ? searchModel.protocols.add(Protocol.http) : searchModel.protocols.remove(Protocol.http);\n          }),\n        ),\n        FilterChip(\n          label: const Text('HTTPS'),\n          selected: searchModel.protocols.contains(Protocol.https),\n          showCheckmark: false,\n          selectedColor: primaryColor.withValues(alpha: 0.12),\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),\n          labelStyle: const TextStyle(fontSize: 12),\n          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n          visualDensity: const VisualDensity(horizontal: -3, vertical: -3),\n          onSelected: (sel) => setState(() {\n            sel ? searchModel.protocols.add(Protocol.https) : searchModel.protocols.remove(Protocol.https);\n          }),\n        ),\n        FilterChip(\n          label: const Text('WS'),\n          selected: searchModel.protocols.contains(Protocol.ws),\n          showCheckmark: false,\n          selectedColor: primaryColor.withValues(alpha: 0.12),\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),\n          labelStyle: const TextStyle(fontSize: 12),\n          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n          visualDensity: const VisualDensity(horizontal: -3, vertical: -3),\n          onSelected: (sel) => setState(() {\n            sel ? searchModel.protocols.add(Protocol.ws) : searchModel.protocols.remove(Protocol.ws);\n          }),\n        ),\n        FilterChip(\n          label: const Text('HTTP/1'),\n          selected: searchModel.protocols.contains(Protocol.http1),\n          showCheckmark: false,\n          selectedColor: primaryColor.withValues(alpha: 0.12),\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),\n          labelStyle: const TextStyle(fontSize: 12),\n          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n          visualDensity: const VisualDensity(horizontal: -3, vertical: -3),\n          onSelected: (sel) => setState(() {\n            sel ? searchModel.protocols.add(Protocol.http1) : searchModel.protocols.remove(Protocol.http1);\n          }),\n        ),\n        FilterChip(\n          label: const Text('H2'),\n          selected: searchModel.protocols.contains(Protocol.h2),\n          showCheckmark: false,\n          selectedColor: primaryColor.withValues(alpha: 0.12),\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),\n          labelStyle: const TextStyle(fontSize: 12),\n          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n          visualDensity: const VisualDensity(horizontal: -3, vertical: -3),\n          onSelected: (sel) => setState(() {\n            sel ? searchModel.protocols.add(Protocol.h2) : searchModel.protocols.remove(Protocol.h2);\n          }),\n        ),\n      ],\n    );\n  }\n\n  Widget options(String title, Option option) {\n    bool isCN = localizations.localeName == 'zh';\n    return Container(\n        constraints: BoxConstraints(maxWidth: isCN ? 100 : 132, minWidth: 100, maxHeight: 33),\n        child: Row(children: [\n          Text(title, style: const TextStyle(fontSize: 12)),\n          Checkbox(\n              visualDensity: VisualDensity.compact,\n              value: searchModel.searchOptions.contains(option),\n              onChanged: (val) {\n                setState(() {\n                  val == true ? searchModel.searchOptions.add(option) : searchModel.searchOptions.remove(option);\n                });\n              })\n        ]));\n  }\n\n  Widget row(Widget child, Widget child2) {\n    return Row(\n        mainAxisAlignment: MainAxisAlignment.end,\n        crossAxisAlignment: CrossAxisAlignment.center,\n        children: [Expanded(flex: 4, child: child), Expanded(flex: 6, child: child2)]);\n  }\n\n  Widget textField({String? initialValue, final ValueChanged<String>? onChanged, TextStyle? style}) {\n    Color color = Theme.of(context).colorScheme.primary;\n\n    return ConstrainedBox(\n        constraints: const BoxConstraints(maxHeight: 32),\n        child: TextFormField(\n          keyboardType: TextInputType.number,\n          inputFormatters: [FilteringTextInputFormatter.digitsOnly],\n          onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n          initialValue: initialValue,\n          onChanged: onChanged,\n          style: style,\n          decoration: InputDecoration(\n            contentPadding: const EdgeInsets.only(left: 10, right: 10, top: 2, bottom: 2),\n            border: OutlineInputBorder(borderSide: BorderSide(width: 1, color: color.withOpacity(0.3))),\n          ),\n        ));\n  }\n}\n\nclass DropdownMenu<T> extends StatefulWidget {\n  final String? initialValue;\n  final Iterable<String> items;\n  final Function(String value) onSelected;\n\n  const DropdownMenu({super.key, this.initialValue, required this.items, required this.onSelected});\n\n  @override\n  State<StatefulWidget> createState() {\n    return DropdownMenuState();\n  }\n}\n\nclass DropdownMenuState extends State<DropdownMenu> {\n  String? selectValue;\n\n  @override\n  void initState() {\n    super.initState();\n    selectValue = widget.initialValue;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return PopupMenuButton(\n      tooltip: '',\n      initialValue: selectValue,\n      child: Wrap(runAlignment: WrapAlignment.center, children: [\n        Text(selectValue ?? '', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),\n        const Icon(Icons.arrow_drop_down, size: 20)\n      ]),\n      onSelected: (String value) {\n        setState(() {\n          widget.onSelected.call(value);\n          selectValue = value;\n        });\n      },\n      itemBuilder: (BuildContext context) {\n        return widget.items\n            .map((it) =>\n                PopupMenuItem<String>(height: 35, value: it, child: Text(it, style: const TextStyle(fontSize: 12))))\n            .toList();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/split_view.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass VerticalSplitView extends StatefulWidget {\n  final Widget left;\n  final Widget right;\n  final double ratio;\n  final double minRatio;\n  final double maxRatio;\n  final Function(double ratio)? onRatioChanged;\n\n  const VerticalSplitView(\n      {super.key,\n      required this.left,\n      required this.right,\n      this.ratio = 0.5,\n      this.minRatio = 0,\n      this.maxRatio = 1,\n      this.onRatioChanged})\n      : assert(ratio >= 0 && ratio <= 1);\n\n  @override\n  State<VerticalSplitView> createState() => _VerticalSplitViewState();\n}\n\nclass _VerticalSplitViewState extends State<VerticalSplitView> {\n  final _dividerWidth = 10.0;\n\n  //from 0-1\n  late double _ratio;\n  double _maxWidth = double.infinity;\n\n  get _width1 => _ratio * _maxWidth;\n\n  get _width2 => (1 - _ratio) * _maxWidth;\n\n  @override\n  void initState() {\n    super.initState();\n    _ratio = widget.ratio;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(builder: (context, BoxConstraints constraints) {\n      if (_maxWidth != constraints.maxWidth) {\n        _maxWidth = constraints.maxWidth - _dividerWidth;\n      }\n\n      return SizedBox(\n        width: constraints.maxWidth,\n        child: Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: <Widget>[\n            SizedBox(\n              width: _width1 - 5,\n              child: widget.left,\n            ),\n            GestureDetector(\n              behavior: HitTestBehavior.translucent,\n              child: MouseRegion(\n                  cursor: SystemMouseCursors.resizeColumn,\n                  child: SizedBox(\n                    width: _dividerWidth,\n                    height: double.infinity,\n                    child: (_ratio <= 0 || _ratio >= 1)\n                        ? const Icon(Icons.drag_handle, size: 16)\n                        : const VerticalDivider(thickness: 1),\n                  )),\n              onPanEnd: (DragEndDetails details) {\n                widget.onRatioChanged?.call(_ratio);\n              },\n              onPanUpdate: (DragUpdateDetails details) {\n                setState(() {\n                  _ratio += details.delta.dx / _maxWidth;\n\n                  if (_ratio > widget.maxRatio) {\n                    _ratio = widget.maxRatio;\n                  } else if (_ratio < widget.minRatio) {\n                    _ratio = widget.minRatio;\n                  }\n                });\n              },\n            ),\n            SizedBox(\n              width: _width2,\n              child: widget.right,\n            ),\n          ],\n        ),\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/state_component.dart",
    "content": "﻿import 'package:flutter/material.dart';\n\nclass KeepAliveWrapper extends StatefulWidget {\n  const KeepAliveWrapper({super.key, this.keepAlive = true, required this.child});\n  final bool keepAlive;\n  final Widget child;\n\n  @override\n  State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();\n}\n\nclass _KeepAliveWrapperState extends State<KeepAliveWrapper> with AutomaticKeepAliveClientMixin {\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return widget.child;\n  }\n\n  @override\n  void didUpdateWidget(covariant KeepAliveWrapper oldWidget) {\n    if (oldWidget.keepAlive != widget.keepAlive) {\n      // keepAlive 状态需要更新，实现在 AutomaticKeepAliveClientMixin 中\n      updateKeepAlive();\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  bool get wantKeepAlive => widget.keepAlive && mounted;\n}\n"
  },
  {
    "path": "lib/ui/component/text_field.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'package:flutter/material.dart';\n\n/// 匹配文本高亮\n/// @author: Hongen Wang\nclass HighlightTextEditingController extends TextEditingController {\n  RegExp? highlightPattern;\n  String? splitPattern;\n\n  //\n  bool highlightEnabled = true;\n\n  HighlightTextEditingController({super.text});\n\n  bool highlight(String? value, {bool caseSensitive = true}) {\n    highlightPattern = value == null ? null : RegExp(value, caseSensitive: caseSensitive);\n    return highlightPattern?.hasMatch(text) ?? false;\n  }\n\n  @override\n  TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) {\n    final text = this.text;\n\n    if (!highlightEnabled || highlightPattern == null || !highlightPattern!.hasMatch(text)) {\n      return super.buildTextSpan(context: context, style: style, withComposing: withComposing);\n    }\n\n    Color color = Theme.of(context).colorScheme.primary;\n    final highlightStyle = style?.copyWith(color: color);\n    final normalStyle = style;\n    List<TextSpan> spans = [];\n    if (splitPattern != null) {\n      var texts = text.split(splitPattern!);\n      for (var i = 0; i < texts.length; i++) {\n        matchHighlight(texts[i], spans, normalStyle: normalStyle, highlightStyle: highlightStyle);\n        spans.add(TextSpan(text: splitPattern, style: normalStyle));\n      }\n    } else {\n      matchHighlight(text, spans, normalStyle: normalStyle, highlightStyle: highlightStyle);\n    }\n    return TextSpan(children: spans, style: style);\n  }\n\n  matchHighlight(String text, List<TextSpan> spans, {TextStyle? normalStyle, TextStyle? highlightStyle}) {\n    int start = 0;\n    for (final match in highlightPattern!.allMatches(text)) {\n      if (match.start > start) {\n        spans.add(TextSpan(text: text.substring(start, match.start), style: normalStyle));\n      }\n      spans.add(TextSpan(text: match.group(0), style: highlightStyle));\n      start = match.end;\n    }\n\n    if (start < text.length) {\n      spans.add(TextSpan(text: text.substring(start), style: normalStyle));\n    }\n  }\n}\n\nInputDecoration decoration(BuildContext context, {String? label, String? hintText, Widget? suffixIcon, bool? isDense}) {\n  Color color = Theme.of(context).colorScheme.primary;\n  return InputDecoration(\n      floatingLabelBehavior: FloatingLabelBehavior.always,\n      labelText: label,\n      hintText: hintText,\n      suffixIcon: suffixIcon,\n      isDense: isDense,\n      hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 15),\n      border: OutlineInputBorder(borderSide: BorderSide(width: 0.8, color: color)),\n      enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.3, color: color)),\n      focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color)));\n}\n"
  },
  {
    "path": "lib/ui/component/transition.dart",
    "content": "import 'package:flutter/material.dart';\n\n///颜色过渡动画\nclass ColorTransition extends StatefulWidget {\n  final Color begin;\n  final Color end;\n  final Duration duration;\n  final Widget child;\n  final bool startAnimation;\n\n  const ColorTransition(\n      {super.key,\n      required this.begin,\n      this.end = Colors.transparent,\n      this.duration = const Duration(milliseconds: 1000),\n      required this.child,\n      this.startAnimation = true});\n\n  @override\n  State<ColorTransition> createState() {\n    return ColorTransitionState();\n  }\n}\n\nclass ColorTransitionState extends State<ColorTransition> with SingleTickerProviderStateMixin {\n  late AnimationController _animationController;\n  late Animation _animation;\n\n  @override\n  void initState() {\n    super.initState();\n\n    //创建动画控制器\n    _animationController = AnimationController(\n      vsync: this,\n      duration: widget.duration,\n    );\n\n    //添加动画执行刷新监听\n    _animationController.addListener(() {\n      setState(() {});\n    });\n\n    //颜色动画变化\n    _animation = ColorTween(begin: widget.begin, end: widget.end).animate(_animationController);\n\n    if (widget.startAnimation) {\n      //延迟150毫秒执行动画\n      Future.delayed(const Duration(milliseconds: 150), () {\n        _animationController.forward();\n      });\n    } else {\n      _animationController.value = _animationController.upperBound;\n    }\n  }\n\n  show() {\n    _animationController.reset();\n    _animationController.forward();\n  }\n\n  @override\n  void dispose() {\n    _animationController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      color: _animation.value,\n      child: widget.child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/component/utils.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:io';\nimport 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/http/content_type.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nimport '../../utils/platform.dart';\n\nconst contentMap = {\n  ContentType.json: Icons.data_object,\n  ContentType.html: Icons.html,\n  ContentType.js: Icons.javascript,\n  ContentType.image: Icons.image,\n  ContentType.video: Icons.video_call,\n  ContentType.text: Icons.text_fields,\n  ContentType.css: Icons.css,\n  ContentType.font: Icons.font_download,\n  ContentType.sse: Icons.stream,\n};\n\nWidget getIcon(HttpResponse? response, {Color? color}) {\n  if (response == null) {\n    return SizedBox(width: 18, child: Icon(Icons.question_mark, size: 16, color: color ?? Colors.green));\n  }\n  if (response.status.code < 0) {\n    return SizedBox(width: 18, child: Icon(Icons.error, size: 16, color: color ?? Colors.red));\n  }\n\n  var contentType = response.contentType;\n  if (contentType.isImage && response.body != null) {\n    return Image.memory(\n      Uint8List.fromList(response.body!),\n      width: Platforms.isDesktop() ? 19 : 26,\n      errorBuilder: (context, error, stackTrace) => Icon(Icons.image, size: 16, color: color ?? Colors.green),\n    );\n  }\n\n  return SizedBox(\n      width: 18, child: Icon(contentMap[contentType] ?? Icons.http, size: 16, color: color ?? Colors.green));\n}\n\n//展示报文大小\nString getPackagesSize(HttpRequest request, HttpResponse? response) {\n  var package = getPackage(request.packageSize);\n  var responsePackage = getPackage(response?.packageSize);\n  if (responsePackage.isEmpty) {\n    return package;\n  }\n  return \"$package / $responsePackage \";\n}\n\nString getPackage(int? size) {\n  if (size == null) {\n    return \"\";\n  }\n  if (size < 1025) {\n    return \"$size B\";\n  }\n\n  if (size > 1024 * 1024) {\n    return \"${(size / 1024 / 1024).toStringAsFixed(2)} M\";\n  }\n  return \"${(size / 1024).toStringAsFixed(2)} K\";\n}\n\nString copyRawRequest(HttpRequest request) {\n  var sb = StringBuffer();\n  var uri = request.requestUri!;\n  var pathAndQuery = uri.path + (uri.query.isNotEmpty ? '?${uri.query}' : '');\n\n  sb.writeln(\"${request.method.name} $pathAndQuery ${request.protocolVersion}\");\n  sb.write(request.headers.headerLines());\n  if (request.bodyAsString.isNotEmpty) {\n    sb.writeln();\n    sb.write(request.bodyAsString);\n  }\n  return sb.toString();\n}\n\nString copyRequest(HttpRequest request, HttpResponse? response) {\n  var sb = StringBuffer();\n  sb.writeln(\"Request\");\n  sb.writeln(\"${request.method.name} ${request.requestUrl} ${request.protocolVersion}\");\n  sb.writeln(request.headers.headerLines());\n  sb.writeln();\n  sb.writeln(request.bodyAsString);\n\n  sb.writeln(\"--------------------------------------------------------\");\n  sb.writeln();\n  sb.writeln(\"Response\");\n  sb.writeln(\"${response?.protocolVersion} ${response?.status.code}\");\n  sb.writeln(response?.headers.headerLines());\n  sb.writeln(response?.bodyAsString);\n  return sb.toString();\n}\n\nRelativeRect menuPosition(BuildContext context) {\n  final RenderBox bar = context.findRenderObject() as RenderBox;\n  final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox;\n  const Offset offset = Offset.zero;\n  final RelativeRect position = RelativeRect.fromRect(\n    Rect.fromPoints(\n      bar.localToGlobal(bar.size.centerRight(offset), ancestor: overlay),\n      bar.localToGlobal(bar.size.centerRight(offset), ancestor: overlay),\n    ),\n    offset & overlay.size,\n  );\n  return position;\n}\n\nWidget contextMenu(BuildContext context, EditableTextState editableTextState, {ContextMenuButtonItem? customItem}) {\n  List<ContextMenuButtonItem> list = [\n    ContextMenuButtonItem(\n      onPressed: () {\n        editableTextState.copySelection(SelectionChangedCause.tap);\n\n        FlutterToastr.show(AppLocalizations.of(context)!.copied, context);\n        unSelect(editableTextState);\n\n        editableTextState.hideToolbar();\n      },\n      type: ContextMenuButtonType.copy,\n    ),\n    ContextMenuButtonItem(\n      label: Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh') ? '复制值' : 'Copy Value',\n      onPressed: () {\n        unSelect(editableTextState);\n        Clipboard.setData(ClipboardData(text: editableTextState.textEditingValue.text)).then((value) {\n          if (context.mounted) FlutterToastr.show(AppLocalizations.of(context)!.copied, context);\n          editableTextState.hideToolbar();\n        });\n      },\n      type: ContextMenuButtonType.custom,\n    ),\n    ContextMenuButtonItem(\n      onPressed: () {\n        editableTextState.selectAll(SelectionChangedCause.tap);\n      },\n      type: ContextMenuButtonType.selectAll,\n    ),\n  ];\n\n  if (customItem != null) {\n    list.add(customItem);\n  }\n\n  if (Platform.isIOS) {\n    list.add(ContextMenuButtonItem(\n      onPressed: () async {\n        editableTextState.shareSelection(SelectionChangedCause.toolbar);\n      },\n      type: ContextMenuButtonType.share,\n    ));\n  }\n\n  return AdaptiveTextSelectionToolbar.buttonItems(\n    anchors: editableTextState.contextMenuAnchors,\n    buttonItems: list,\n  );\n}\n\nvoid unSelect(EditableTextState editableTextState) {\n  editableTextState.userUpdateTextEditingValue(\n    editableTextState.textEditingValue\n        .copyWith(selection: TextSelection.collapsed(offset: editableTextState.textEditingValue.selection.baseOffset)),\n    SelectionChangedCause.tap,\n  );\n}\n\n///Future\nWidget futureWidget<T>(Future<T> future, Widget Function(T data) toWidget, {T? initialData, bool loading = false}) {\n  return FutureBuilder<T>(\n    future: future,\n    initialData: initialData,\n    builder: (BuildContext context, AsyncSnapshot<T> snapshot) {\n      if (snapshot.data != null) {\n        return toWidget(snapshot.requireData);\n      }\n\n      if (snapshot.connectionState == ConnectionState.done) {\n        if (snapshot.hasError) {\n          logger.e(snapshot.error);\n        }\n        return toWidget(snapshot.requireData);\n      }\n      //加载效果\n      return loading ? const Center(child: CircularProgressIndicator()) : const SizedBox();\n    },\n  );\n}\n\nFuture showContextMenu(BuildContext context, Offset offset, {required List<PopupMenuEntry> items}) {\n  return showMenu(\n      context: context,\n      position: RelativeRect.fromLTRB(\n        offset.dx + 10,\n        offset.dy - 50,\n        offset.dx + 10,\n        offset.dy - 50,\n      ),\n      items: items);\n}\n\nFuture<T?> showConfirmDialog<T>(BuildContext context, {String? title, String? content, VoidCallback? onConfirm}) {\n  title ??= AppLocalizations.of(context)!.confirmTitle;\n  content ??= AppLocalizations.of(context)!.confirmContent;\n  return showDialog(\n      context: context,\n      builder: (context) {\n        return AlertDialog(\n          title: Text(title!, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),\n          content: Text(content!),\n          actions: [\n            TextButton(\n              onPressed: () => Navigator.pop(context),\n              child: Text(AppLocalizations.of(context)!.cancel),\n            ),\n            TextButton(\n              onPressed: () {\n                Navigator.pop(context);\n                if (onConfirm != null) onConfirm();\n              },\n              child: Text(AppLocalizations.of(context)!.confirm),\n            ),\n          ],\n        );\n      });\n}\n\n///滚动条\nScrollController? trackingScroll(ScrollController? scrollController) {\n  if (scrollController == null) {\n    return null;\n  }\n\n  var trackingScroll = TrackingScrollController();\n  double offset = 0;\n  trackingScroll.addListener(() {\n    if (trackingScroll.offset < 30 && trackingScroll.offset < offset && scrollController.offset > 0) {\n      //往上滚动\n      scrollController.jumpTo(scrollController.offset - max(offset - trackingScroll.offset, 15));\n    } else if (trackingScroll.offset > 0 &&\n        trackingScroll.offset > offset &&\n        scrollController.offset < scrollController.position.maxScrollExtent) {\n      //往下滚动\n      scrollController.jumpTo(scrollController.offset + max(trackingScroll.offset - offset, 15));\n    }\n\n    offset = trackingScroll.offset;\n  });\n  return trackingScroll;\n}\n"
  },
  {
    "path": "lib/ui/component/widgets.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nclass CustomPopupMenuItem<T> extends PopupMenuItem<T> {\n  final Color? color;\n\n  const CustomPopupMenuItem({\n    super.key,\n    super.onTap,\n    super.height,\n    super.value,\n    super.enabled,\n    super.padding,\n    required Widget super.child,\n    this.color,\n  });\n\n  @override\n  PopupMenuItemState<T, CustomPopupMenuItem<T>> createState() => _CustomPopupMenuItemState<T>();\n}\n\nclass _CustomPopupMenuItemState<T> extends PopupMenuItemState<T, CustomPopupMenuItem<T>> {\n  @override\n  Widget build(BuildContext context) {\n    return Theme(\n      data: Theme.of(context).copyWith(\n        hoverColor: Theme.of(context).focusColor,\n      ),\n      child: super.build(context),\n    );\n  }\n}\n\nclass PopupMenuContainer extends PopupMenuEntry {\n  final Widget child;\n\n  @override\n  final double height;\n\n  const PopupMenuContainer({super.key, required this.child, this.height = 40});\n\n  @override\n  bool represents(value) => false;\n\n  @override\n  State<StatefulWidget> createState() => _PopupMenuContainerState();\n}\n\nclass _PopupMenuContainerState extends State<PopupMenuContainer> {\n  @override\n  Widget build(BuildContext context) {\n    return widget.child;\n  }\n}\n\nclass SwitchWidget extends StatefulWidget {\n  final String? title;\n  final String? subtitle;\n  final ValueWrap<bool> value;\n  final ValueChanged<bool> onChanged;\n  final double scale;\n\n  SwitchWidget({super.key, this.title, this.subtitle, required bool value, required this.onChanged, this.scale = 1})\n      : value = ValueWrap.of(value);\n\n  @override\n  State<StatefulWidget> createState() => _SwitchState();\n}\n\nclass _SwitchState extends State<SwitchWidget> {\n  @override\n  Widget build(BuildContext context) {\n    if (widget.title == null) {\n      return Transform.scale(\n          scale: widget.scale,\n          child: Switch(\n            value: widget.value.get() == true,\n            onChanged: (value) {\n              setState(() {\n                widget.value.set(value);\n              });\n              widget.onChanged(value);\n            },\n          ));\n    }\n    return Transform.scale(\n        scale: widget.scale,\n        child: SwitchListTile(\n          title: widget.title == null ? null : Text(widget.title!),\n          subtitle: widget.subtitle == null ? null : Text(widget.subtitle!),\n          value: widget.value.get() == true,\n          dense: true,\n          onChanged: (value) {\n            setState(() {\n              widget.value.set(value);\n            });\n            widget.onChanged(value);\n          },\n        ));\n  }\n}\n\nclass Dot extends StatelessWidget {\n  final Color? color;\n  final double size;\n\n  const Dot({super.key, this.color = const Color(0xFF00FF00), this.size = 5});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: size,\n      height: size,\n      decoration: BoxDecoration(color: color, shape: BoxShape.circle),\n    );\n  }\n}\n\nclass BottomSheetItem extends StatelessWidget {\n  final String text;\n  final VoidCallback? onPressed;\n\n  const BottomSheetItem({super.key, required this.text, this.onPressed});\n\n  @override\n  Widget build(BuildContext context) {\n    return TextButton(\n        onPressed: () {\n          Navigator.of(context).pop();\n          onPressed?.call();\n        },\n        child: SizedBox(width: double.infinity, child: Text(text, textAlign: TextAlign.center)));\n  }\n}\n\nclass IconText extends StatelessWidget {\n  final GestureTapCallback? onTap;\n  final Icon? icon;\n  final Widget? trailing;\n  final String text;\n  final TextStyle? textStyle;\n\n  const IconText({super.key, this.onTap, required this.text, this.icon, this.trailing, this.textStyle});\n\n  @override\n  Widget build(BuildContext context) {\n    return InkWell(\n        onTap: onTap,\n        child: Row(children: [\n          if (icon != null) icon!,\n          if (icon != null) const SizedBox(width: 8),\n          Expanded(child: Text(text, style: textStyle)),\n          if (trailing != null) trailing!\n        ]));\n  }\n}\n\nclass LazyIndexedStack extends StatefulWidget {\n  final List<Widget> children;\n  final int index;\n\n  const LazyIndexedStack({\n    super.key,\n    required this.children,\n    required this.index,\n  });\n\n  @override\n  State<LazyIndexedStack> createState() => _LazyIndexedStackState();\n}\n\nclass _LazyIndexedStackState extends State<LazyIndexedStack> {\n  final List<Widget?> _childrenCache = [];\n\n  @override\n  void initState() {\n    super.initState();\n    _childrenCache.length = widget.children.length;\n  }\n\n  @override\n  void didUpdateWidget(LazyIndexedStack oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (oldWidget.children.length != widget.children.length) {\n      _childrenCache.length = widget.children.length;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    _childrenCache[widget.index] ??= widget.children[widget.index];\n\n    return IndexedStack(\n      index: widget.index,\n      children: List.generate(\n        widget.children.length,\n        (i) => _childrenCache[i] ?? Container(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/configuration.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:path_provider/path_provider.dart';\n\n/// @author wanghongen\n/// 2024/1/1\nclass ColorMapping {\n  static final Map<String, Color> colors = {\n    \"Blue\": Colors.blue,\n    \"Pink\": Colors.pink,\n    \"Red\": Colors.red,\n    \"Purple\": Colors.deepPurple,\n    \"Green\": Colors.green,\n    \"Teal\": Colors.teal,\n    \"Cyan\": Colors.cyan,\n    \"Orange\": Colors.orange,\n    \"Yellow\": Colors.yellow[900]!,\n    \"Grey\": Colors.grey,\n  };\n\n  static Color getColor(String colorName) {\n    return colors[colorName] ?? Colors.blue;\n  }\n\n  static String getColorName(Color color) {\n    return colors.entries.firstWhere((entry) => entry.value == color).key;\n  }\n}\n\nclass ThemeModel {\n  ThemeMode mode;\n  bool useMaterial3;\n  String color = \"Pink\";\n\n  ThemeModel({this.mode = ThemeMode.system, this.useMaterial3 = true});\n\n  ThemeModel copy({ThemeMode? mode, bool? useMaterial3}) => ThemeModel(\n        mode: mode ?? this.mode,\n        useMaterial3: useMaterial3 ?? this.useMaterial3,\n      );\n\n  Color get themeColor => ColorMapping.colors[color] ?? Colors.blue;\n}\n\nclass AppConfiguration {\n  static const String version = \"1.2.6\";\n\n  ValueNotifier<bool> globalChange = ValueNotifier(false);\n\n  ThemeModel _theme = ThemeModel();\n  Locale? _language;\n\n  //是否显示更新内容公告\n  bool upgradeNoticeV26 = true;\n\n  /// 是否启用画中画\n  ValueNotifier<bool> pipEnabled = ValueNotifier(Platform.isAndroid);\n\n  /// 显示画中画图标\n  ValueNotifier<bool> pipIcon = ValueNotifier(Platform.isAndroid);\n\n  /// header默认展示\n  bool headerExpanded = true;\n\n  /// Headers展示模式: table(逐行) / text(原始文本)\n  String headerViewMode = \"table\";\n\n  /// 底部导航栏\n  bool bottomNavigation = true;\n\n  /// 内存清理\n  int? memoryCleanupThreshold;\n\n  ///自动已读\n  bool autoReadEnabled = true;\n\n  //桌面window大小\n  Size? windowSize;\n\n  //桌面window位置\n  Offset? windowPosition;\n\n  //左侧面板占比\n  double panelRatio = 0.3;\n\n  AppConfiguration._();\n\n  /// 单例\n  static AppConfiguration? _instance;\n\n  static Future<AppConfiguration> get instance async {\n    if (_instance == null) {\n      try {\n        AppConfiguration configuration = AppConfiguration._();\n        await configuration.initConfig();\n        _instance = configuration;\n      } catch (e) {\n        logger.e(\"load config error: $e\");\n        _instance = AppConfiguration._();\n      }\n    }\n    return _instance!;\n  }\n\n  static AppConfiguration? get current => _instance;\n\n  ThemeMode get themeMode => _theme.mode;\n\n  set themeMode(ThemeMode mode) {\n    if (mode == _theme.mode) return;\n    _theme.mode = mode;\n    globalChange.value = !globalChange.value;\n    flushConfig();\n  }\n\n  ///Material3\n  bool get useMaterial3 => _theme.useMaterial3;\n\n  set useMaterial3(bool value) {\n    if (value == useMaterial3) return;\n    _theme.useMaterial3 = value;\n    globalChange.value = !globalChange.value;\n    flushConfig();\n  }\n\n  Color get themeColor => _theme.themeColor;\n\n  set setThemeColor(String colorName) {\n    var color = ColorMapping.colors[colorName];\n    if (color == null || color == themeColor) return;\n\n    _theme.color = colorName;\n    globalChange.value = !globalChange.value;\n    flushConfig();\n  }\n\n  ///language\n  Locale? get language => _language;\n\n  set language(Locale? locale) {\n    if (locale == _language) return;\n    _language = locale;\n    globalChange.value = !globalChange.value;\n    flushConfig();\n  }\n\n  Future<File> get _path async {\n    if (Platforms.isDesktop()) {\n      var userHome = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];\n      return File('$userHome${Platform.pathSeparator}.proxypin${Platform.pathSeparator}ui_config.json');\n    }\n\n    final directory = await getApplicationSupportDirectory();\n    var file = File('${directory.path}${Platform.pathSeparator}ui_config.json');\n    if (!await file.exists()) {\n      await file.create();\n    }\n    return file;\n  }\n\n  /// 初始化配置\n  Future<void> initConfig() async {\n    // 读取配置文件\n    var file = await _path;\n    logger.d(file);\n    var exits = await file.exists();\n    if (!exits) {\n      return;\n    }\n    var json = await file.readAsString();\n    if (json.isEmpty) {\n      return;\n    }\n\n    try {\n      Map<String, dynamic> config = jsonDecode(json);\n      var mode =\n          ThemeMode.values.firstWhere((element) => element.name == config['mode'], orElse: () => ThemeMode.system);\n      _theme = ThemeModel(mode: mode, useMaterial3: config['useMaterial3'] ?? true);\n      _theme.color = config['themeColor'] ?? \"Blue\";\n\n      upgradeNoticeV26 = config['upgradeNoticeV26'] ?? true;\n      _language = config['language'] == null \n        ? null \n        : Locale.fromSubtags(\n            languageCode: config['language'], \n            scriptCode: config['languageScript']\n          );\n      pipEnabled.value = config['pipEnabled'] ?? true;\n      pipIcon.value = config['pipIcon'] ?? false;\n      headerExpanded = config['headerExpanded'] ?? true;\n      headerViewMode = config['headerViewMode'] ?? \"table\";\n      bottomNavigation = config['bottomNavigation'] ?? true;\n      memoryCleanupThreshold = config['memoryCleanupThreshold'];\n      autoReadEnabled = config['autoReadEnabled'] ?? true;\n\n      windowSize =\n          config['windowSize'] == null ? null : Size(config['windowSize']['width'], config['windowSize']['height']);\n      windowPosition = config['windowPosition'] == null\n          ? null\n          : Offset(config['windowPosition']['dx'], config['windowPosition']['dy']);\n      if (config['panelRatio'] != null) {\n        panelRatio = config['panelRatio'];\n      }\n    } catch (e) {\n      logger.e(e);\n    }\n  }\n\n  /// 是否正在写入\n  bool _isWriting = false;\n\n  /// 刷新配置文件\n  Future<void> flushConfig() async {\n    if (_isWriting) return;\n    _isWriting = true;\n\n    var file = await _path;\n    var exists = await file.exists();\n    if (!exists) {\n      file = await file.create(recursive: true);\n    }\n\n    var json = jsonEncode(toJson());\n    await file.writeAsString(json);\n    _isWriting = false;\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'mode': _theme.mode.name,\n      'themeColor': _theme.color,\n      'useMaterial3': _theme.useMaterial3,\n      'upgradeNoticeV26': upgradeNoticeV26,\n      \"language\": _language?.languageCode,\n      \"languageScript\": _language?.scriptCode,\n      \"headerExpanded\": headerExpanded,\n      \"headerViewMode\": headerViewMode,\n      \"autoReadEnabled\": autoReadEnabled,\n      if (memoryCleanupThreshold != null) 'memoryCleanupThreshold': memoryCleanupThreshold,\n      if (Platforms.isMobile()) 'pipEnabled': pipEnabled.value,\n      if (Platforms.isMobile()) 'pipIcon': pipIcon.value ? true : null,\n      if (Platforms.isMobile()) 'bottomNavigation': bottomNavigation,\n      if (Platforms.isDesktop())\n        \"windowSize\": windowSize == null ? null : {\"width\": windowSize?.width, \"height\": windowSize?.height},\n      if (Platforms.isDesktop())\n        \"windowPosition\": windowPosition == null ? null : {\"dx\": windowPosition?.dx, \"dy\": windowPosition?.dy},\n      if (Platforms.isDesktop()) 'panelRatio': panelRatio,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/ui/content/body.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:math';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:image_pickers/image_pickers.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/network/http/content_type.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/json/json_viewer.dart';\nimport 'package:proxypin/ui/component/json/theme.dart';\nimport 'package:proxypin/ui/component/multi_window.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/desktop/setting/request_rewrite.dart';\nimport 'package:proxypin/ui/mobile/setting/request_rewrite.dart';\nimport 'package:proxypin/utils/crypto_body_decoder.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/num.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:window_manager/window_manager.dart';\n\nimport '../component/json/json_text.dart';\nimport '../component/search/highlight_text.dart';\nimport '../component/search/search_controller.dart';\nimport '../toolbox/encoder.dart';\n\n///请求响应的body部分\n///@Author wanghongen\nclass HttpBodyWidget extends StatefulWidget {\n  final HttpMessage? httpMessage;\n  final bool inNewWindow; //是否在新窗口打开\n  final WindowController? windowController;\n  final ScrollController? scrollController;\n  final bool hideRequestRewrite; //是否隐藏请求重写\n\n  const HttpBodyWidget(\n      {super.key,\n      required this.httpMessage,\n      this.inNewWindow = false,\n      this.windowController,\n      this.scrollController,\n      this.hideRequestRewrite = false});\n\n  @override\n  State<StatefulWidget> createState() {\n    return HttpBodyState();\n  }\n}\n\nclass HttpBodyState extends State<HttpBodyWidget> {\n  var bodyKey = GlobalKey<_BodyState>();\n  int tabIndex = 0;\n  final searchIconKey = GlobalKey();\n  final SearchTextController searchController = SearchTextController();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n  bool showDecoded = false;\n  CryptoDecodedResult? decoded;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.windowController != null) {\n      HardwareKeyboard.instance.addHandler(onKeyEvent);\n    }\n\n    _loadDecoded();\n  }\n\n  @override\n  void didUpdateWidget(covariant HttpBodyWidget oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (oldWidget.httpMessage?.requestId != widget.httpMessage?.requestId) {\n      showDecoded = false;\n      decoded = null;\n      _loadDecoded();\n    }\n  }\n\n  /// 按键事件\n  bool onKeyEvent(KeyEvent event) {\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      widget.windowController?.close();\n      return true;\n    }\n\n    return false;\n  }\n\n  Future<void> _loadDecoded() async {\n    final message = widget.httpMessage;\n    if (message == null) return;\n    decoded = await CryptoBodyDecoder.maybeDecode(message);\n    if (mounted) setState(() {});\n  }\n\n  @override\n  void dispose() {\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    searchController.dispose();\n    widget.scrollController?.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.httpMessage == null) {\n      return const SizedBox();\n    }\n\n    if ((widget.httpMessage?.body == null || widget.httpMessage?.body?.isEmpty == true) &&\n        widget.httpMessage?.messages.isNotEmpty == false) {\n      return const SizedBox();\n    }\n\n    var tabs = Tabs.of(widget.httpMessage?.contentType, isJsonText());\n\n    if (tabIndex > 0 && tabIndex >= tabs.list.length) tabIndex = tabs.list.length - 1;\n    bodyKey.currentState?.changeState(widget.httpMessage, tabs.list[tabIndex]);\n\n    //TabBar\n    List<Widget> list = [\n      widget.inNewWindow ? const SizedBox() : titleWidget(),\n      const SizedBox(height: 3),\n      SizedBox(\n          height: 36,\n          child: TabBar(\n              labelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),\n              labelPadding: const EdgeInsets.only(left: 3, right: 5),\n              tabs: tabs.tabList(),\n              onTap: (index) {\n                tabIndex = index;\n                bodyKey.currentState?.changeState(widget.httpMessage, tabs.list[tabIndex]);\n              })),\n      Padding(\n          padding: const EdgeInsets.all(10),\n          child: _Body(\n              key: bodyKey,\n              message: widget.httpMessage,\n              viewType: tabs.list[tabIndex],\n              scrollController: widget.scrollController,\n              searchController: searchController)) //body\n    ];\n\n    var tabController = FocusableActionDetector(\n        shortcuts: {\n          LogicalKeySet(\n                  Platform.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.control, LogicalKeyboardKey.keyF):\n              ActivateIntent(),\n          LogicalKeySet(LogicalKeyboardKey.escape): DismissIntent(),\n        },\n        actions: {\n          ActivateIntent: CallbackAction<ActivateIntent>(\n            onInvoke: (intent) {\n              if (searchController.isSearchOverlayVisible) {\n                hideSearchOverlay();\n              } else {\n                RenderBox renderBox = searchIconKey.currentContext?.findRenderObject() as RenderBox;\n                Offset position = renderBox.localToGlobal(Offset.zero); // 获取搜索图标的位置\n\n                searchController.showSearchOverlay(context,\n                    top: max(position.dy + renderBox.size.height + 50, 100), right: 10);\n              }\n              return null;\n            },\n          ),\n          DismissIntent: CallbackAction<DismissIntent>(\n            onInvoke: (intent) {\n              hideSearchOverlay();\n              return null;\n            },\n          ),\n        },\n        child: DefaultTabController(\n            initialIndex: tabIndex,\n            length: tabs.list.length,\n            child: widget.inNewWindow\n                ? ListView(children: list)\n                : Column(crossAxisAlignment: CrossAxisAlignment.start, children: list)));\n\n    //在新窗口打开\n    if (widget.inNewWindow) {\n      return Scaffold(\n          appBar: AppBar(title: titleWidget(inNewWindow: true), toolbarHeight: Platform.isWindows ? 36 : null),\n          body: tabController);\n    }\n    return tabController;\n  }\n\n  void hideSearchOverlay() {\n    searchController.removeSearchOverlay();\n  }\n\n  //判断是否是json格式\n  bool isJsonText() {\n    var bodyString = widget.httpMessage?.bodyAsString;\n    return bodyString != null &&\n        (bodyString.startsWith('{') && bodyString.endsWith('}') ||\n            bodyString.startsWith('[') && bodyString.endsWith(']'));\n  }\n\n  /// 标题\n  Widget titleWidget({bool inNewWindow = false}) {\n    var type = widget.httpMessage is HttpRequest ? \"Request\" : \"Response\";\n\n    bool isImage = widget.httpMessage?.contentType == ContentType.image;\n    VisualDensity visualDensity = Platforms.isMobile() ? VisualDensity.compact : VisualDensity.standard;\n\n    final isMobile = Platforms.isMobile();\n\n    // Build common actions as widgets so we can either display them inline (desktop)\n    // or move them into an overflow menu (mobile) to avoid hiding important buttons.\n    final searchBtn = InkWell(\n      key: searchIconKey,\n      child: const Icon(Icons.search, size: 20),\n      onTap: () {\n        if (searchController.isSearchOverlayVisible) {\n          searchController.removeSearchOverlay();\n        } else {\n          RenderBox renderBox = searchIconKey.currentContext?.findRenderObject() as RenderBox;\n          Offset position = renderBox.localToGlobal(Offset.zero);\n          searchController.showSearchOverlay(context, top: position.dy + renderBox.size.height + 50, right: 10);\n        }\n      },\n    );\n\n    final copyBtn = isImage\n        ? downloadImageButton()\n        : IconButton(\n            visualDensity: visualDensity,\n            iconSize: 16,\n            icon: const Icon(Icons.copy),\n            tooltip: localizations.copy,\n            onPressed: () async {\n              var body = await bodyKey.currentState?.getBody();\n              if (body == null) return;\n              Clipboard.setData(ClipboardData(text: body)).then((_) {\n                if (mounted) FlutterToastr.show(localizations.copied, context);\n              });\n            },\n          );\n\n    final rewriteBtn = IconButton(\n      visualDensity: visualDensity,\n      iconSize: 16,\n      icon: const Icon(Icons.edit_document),\n      tooltip: localizations.requestRewrite,\n      onPressed: showRequestRewrite,\n    );\n\n    final encodeBtn = IconButton(\n        visualDensity: visualDensity,\n        iconSize: 20,\n        icon: const Icon(Icons.text_format),\n        tooltip: localizations.encode,\n        onPressed: () async {\n          var body = await bodyKey.currentState?.getBody();\n          if (mounted) {\n            encodeWindow(EncoderType.base64, context, body);\n          }\n        });\n\n    final openNewBtn = IconButton(\n        visualDensity: visualDensity,\n        iconSize: 16,\n        icon: const Icon(Icons.open_in_new),\n        tooltip: localizations.newWindow,\n        onPressed: () => openNew());\n\n    Widget? cryptoToggle;\n    if (decoded != null) {\n      cryptoToggle = TextButton.icon(\n        onPressed: () {\n          setState(() {\n            showDecoded = !showDecoded;\n          });\n        },\n        icon: Icon(showDecoded ? Icons.lock_open : Icons.lock, size: 18),\n        label: Text(showDecoded ? localizations.cryptoDecoded : localizations.cryptoDecodeToggle),\n      );\n    }\n\n    // Mobile UX:\n    // - If there is NO crypto result, keep the original (previous) horizontal-scroll title bar.\n    // - Only when crypto is available, switch to the compact overflow-menu layout to keep\n    //   the crypto toggle visible.\n    if (isMobile && cryptoToggle != null) {\n      final overflowItems = <PopupMenuEntry<String>>[];\n      if (!widget.hideRequestRewrite) {\n        overflowItems.add(PopupMenuItem(value: 'rewrite', child: Text(localizations.requestRewrite)));\n      }\n      overflowItems.add(PopupMenuItem(value: 'encode', child: Text(localizations.encode)));\n      if (!inNewWindow) {\n        overflowItems.add(PopupMenuItem(value: 'new_window', child: Text(localizations.newWindow)));\n      }\n\n      return Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Text('$type Body', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n          const SizedBox(width: 8),\n          searchBtn,\n          const SizedBox(width: 4),\n          copyBtn,\n          const SizedBox(width: 4),\n          Flexible(child: cryptoToggle),\n          if (overflowItems.isNotEmpty)\n            PopupMenuButton<String>(\n              icon: const Icon(Icons.more_vert, size: 20),\n              onSelected: (v) {\n                if (v == 'rewrite') showRequestRewrite();\n                if (v == 'encode') {\n                  bodyKey.currentState?.getBody().then((body) {\n                    if (mounted) encodeWindow(EncoderType.base64, context, body);\n                  });\n                }\n                if (v == 'new_window') openNew();\n              },\n              itemBuilder: (_) => overflowItems,\n            ),\n        ],\n      );\n    }\n\n    // Default (desktop + mobile without crypto): keep the previous full inline actions\n    // (horizontal scroll when needed).\n    final list = <Widget>[\n      Text('$type Body', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n      const SizedBox(width: 18),\n      searchBtn,\n      const SizedBox(width: 4),\n      copyBtn,\n    ];\n\n    if (!widget.hideRequestRewrite) {\n      list.add(rewriteBtn);\n    }\n    list.add(encodeBtn);\n    if (!inNewWindow) {\n      list.add(openNewBtn);\n    }\n    if (cryptoToggle != null) {\n      list.add(cryptoToggle);\n    }\n\n    return SingleChildScrollView(scrollDirection: Axis.horizontal, child: Row(children: list));\n  }\n\n  ///下载图片\n  Widget downloadImageButton() {\n    return IconButton(\n        iconSize: 19,\n        visualDensity: VisualDensity.comfortable,\n        icon: Icon(Icons.download),\n        tooltip: localizations.saveImage,\n        onPressed: () async {\n          var body = bodyKey.currentState?.message?.body;\n          if (body == null) {\n            return;\n          }\n          var bytes = Uint8List.fromList(body);\n          if (Platforms.isMobile()) {\n            String? path = await ImagePickers.saveByteDataImageToGallery(bytes);\n            if (path != null && mounted) {\n              FlutterToastr.show(localizations.saveSuccess, context, duration: 2, rootNavigator: true);\n            }\n            return;\n          }\n\n          if (Platforms.isDesktop()) {\n            var fileName = \"image_${DateTime.now().millisecondsSinceEpoch}.png\";\n            String? path = (await FilePicker.platform.saveFile(fileName: fileName));\n            if (path == null) return;\n\n            await File(path).writeAsBytes(bytes);\n            if (mounted) {\n              FlutterToastr.show(localizations.saveSuccess, context, duration: 2);\n            }\n          }\n        });\n  }\n\n  ///展示请求重写\n  Future<void> showRequestRewrite() async {\n    HttpRequest? request;\n    if (widget.httpMessage == null) {\n      return;\n    }\n\n    bool isRequest = widget.httpMessage is HttpRequest;\n    if (widget.httpMessage is HttpRequest) {\n      request = widget.httpMessage as HttpRequest;\n    } else {\n      request = (widget.httpMessage as HttpResponse).request;\n    }\n    var requestRewrites = await RequestRewriteManager.instance;\n\n    var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace;\n    var rule = requestRewrites.getRequestRewriteRule(request!, ruleType);\n\n    var rewriteItems = await requestRewrites.getRewriteItems(rule);\n\n    if (!mounted) return;\n\n    if (Platforms.isMobile()) {\n      Navigator.push(\n          context, MaterialPageRoute(builder: (_) => RewriteRule(rule: rule, items: rewriteItems, request: request)));\n    } else {\n      showDialog(\n              context: context,\n              barrierDismissible: false,\n              builder: (BuildContext context) => RewriteRuleEdit(rule: rule, items: rewriteItems, request: request))\n          .then((value) {\n        if (value is RequestRewriteRule && mounted) {\n          FlutterToastr.show(localizations.saveSuccess, context);\n        }\n      });\n    }\n  }\n\n  ///打开新窗口\n  void openNew() async {\n    if (Platforms.isDesktop()) {\n      var size = MediaQuery.of(context).size;\n      var ratio = 1.0;\n      if (Platform.isWindows) {\n        ratio = WindowManager.instance.getDevicePixelRatio();\n      }\n      final window = await DesktopMultiWindow.createWindow(jsonEncode(\n        {'name': 'HttpBodyWidget', 'httpMessage': widget.httpMessage, 'inNewWindow': true},\n      ));\n      window\n        ..setTitle(widget.httpMessage is HttpRequest ? localizations.requestBody : localizations.responseBody)\n        ..setFrame(const Offset(100, 100) & Size(800 * ratio, size.height * ratio))\n        ..center()\n        ..show();\n      return;\n    }\n\n    Navigator.push(\n        context, MaterialPageRoute(builder: (_) => HttpBodyWidget(httpMessage: widget.httpMessage, inNewWindow: true)));\n  }\n}\n\nclass _Body extends StatefulWidget {\n  final HttpMessage? message;\n  final ViewType viewType;\n  final ScrollController? scrollController;\n  final SearchTextController searchController; // 添加搜索设置控制器\n\n  const _Body({\n    super.key,\n    this.message,\n    required this.viewType,\n    this.scrollController,\n    required this.searchController, // 添加必需参数\n  });\n\n  @override\n  State<StatefulWidget> createState() {\n    return _BodyState();\n  }\n}\n\nclass _BodyState extends State<_Body> {\n  late ViewType viewType;\n  HttpMessage? message;\n\n  @override\n  void initState() {\n    super.initState();\n    viewType = widget.viewType;\n    message = widget.message;\n  }\n\n  void changeState(HttpMessage? message, ViewType viewType) {\n    setState(() {\n      this.message = message;\n      this.viewType = viewType;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return _getBody(viewType);\n  }\n\n  Future<String?> getBody() async {\n    final parent = context.findAncestorStateOfType<HttpBodyState>();\n    if (parent?.showDecoded == true && parent?.decoded?.text != null) {\n      return parent!.decoded!.text;\n    }\n\n    if (message?.isWebSocket == true) {\n      return message?.messages.map((e) => e.payloadDataAsString).join(\"\\n\");\n    }\n\n    if (message == null || message?.body == null) {\n      return null;\n    }\n\n    if (viewType == ViewType.hex) {\n      return message!.body!.map(intToHex).join(\" \");\n    }\n\n    try {\n      if (viewType == ViewType.formUrl) {\n        return Uri.decodeFull(message!.bodyAsString);\n      }\n\n      if (viewType == ViewType.jsonText || viewType == ViewType.json) {\n        //json格式化\n        var jsonObject = json.decode(await message!.decodeBodyString());\n        return const JsonEncoder.withIndent(\"  \").convert(jsonObject);\n      }\n    } catch (_) {}\n    return message!.decodeBodyString();\n  }\n\n  Widget _getBody(ViewType type) {\n    final parent = context.findAncestorStateOfType<HttpBodyState>();\n    final message = parent?.showDecoded == true && parent?.decoded != null\n        ? _DecodedHttpMessage(widget.message!, parent!.decoded!)\n        : widget.message;\n\n    if (message?.isWebSocket == true ||\n        (message?.contentType == ContentType.sse && message?.messages.isNotEmpty == true)) {\n      List<Widget>? list = message?.messages\n          .map((e) => Container(\n              margin: const EdgeInsets.only(top: 2, bottom: 2),\n              child: Row(\n                children: [\n                  Expanded(child: Text(e.payloadDataAsString)),\n                  const SizedBox(width: 5),\n                  SizedBox(\n                      width: 130,\n                      child: SelectionContainer.disabled(\n                          child: Text(e.time.format(), style: const TextStyle(fontSize: 12, color: Colors.grey))))\n                ],\n              )))\n          .toList();\n      return Column(\n        children: [\n          const SelectionContainer.disabled(\n              child: Row(children: [\n            Expanded(child: Text(\"Data\")),\n            SizedBox(width: 130, child: Text(\"Time\")),\n          ])),\n          Divider(height: 5, thickness: 1, color: Colors.grey[300]),\n          ...list ?? []\n        ],\n      );\n    }\n\n    if (message == null || message.body == null) {\n      return const SizedBox();\n    }\n\n    if (type == ViewType.image) {\n      return Center(child: Image.memory(Uint8List.fromList(message.body ?? []), fit: BoxFit.scaleDown));\n    }\n    if (type == ViewType.video) {\n      return const Center(child: Text(\"video not support preview\"));\n    }\n    if (type == ViewType.hex) {\n      return HexViewer(data: Uint8List.fromList(message.body!), searchController: widget.searchController);\n    }\n\n    if (type == ViewType.formUrl) {\n      return HighlightTextWidget(\n          text: Uri.decodeFull(message.getBodyString()),\n          searchController: widget.searchController,\n          contextMenuBuilder: contextMenu);\n    }\n\n    return futureWidget(message.decodeBodyString(), initialData: message.getBodyString(), (body) {\n      try {\n        if (type == ViewType.jsonText) {\n          var jsonObject = json.decode(body);\n          return JsonText(\n              json: jsonObject,\n              indent: Platforms.isDesktop() ? '    ' : '  ',\n              colorTheme: ColorTheme.of(context),\n              searchController: widget.searchController,\n              scrollController: widget.scrollController);\n        }\n\n        if (type == ViewType.json) {\n          return JsonViewer(json.decode(body),\n              colorTheme: ColorTheme.of(context), searchController: widget.searchController);\n        }\n\n        return HighlightTextWidget(\n            text: body, searchController: widget.searchController, contextMenuBuilder: contextMenu);\n      } catch (e) {\n        logger.e(e, stackTrace: StackTrace.current);\n      }\n\n      return HighlightTextWidget(\n          text: body, searchController: widget.searchController, contextMenuBuilder: contextMenu);\n    });\n  }\n}\n\nclass Tabs {\n  final List<ViewType> list = [];\n\n  static Tabs of(ContentType? contentType, bool isJsonText) {\n    var tabs = Tabs();\n    if (contentType == null) {\n      return tabs;\n    }\n\n    if (contentType == ContentType.video) {\n      tabs.list.add(ViewType.video);\n      tabs.list.add(ViewType.hex);\n      return tabs;\n    }\n\n    if (contentType == ContentType.json) {\n      tabs.list.add(ViewType.jsonText);\n    }\n\n    tabs.list.add(ViewType.of(contentType) ?? ViewType.text);\n\n    //为json时，增加json格式化\n    if (isJsonText && !tabs.list.contains(ViewType.jsonText)) {\n      tabs.list.add(ViewType.jsonText);\n      tabs.list.add(ViewType.json);\n    }\n\n    if (contentType == ContentType.formUrl || contentType == ContentType.json) {\n      tabs.list.add(ViewType.text);\n    }\n\n    tabs.list.add(ViewType.hex);\n    return tabs;\n  }\n\n  List<Tab> tabList() {\n    return list.map((e) => Tab(text: e.title)).toList();\n  }\n}\n\nenum ViewType {\n  text(\"Text\"),\n  formUrl(\"URL Decode\"),\n  json(\"JSON\"),\n  jsonText(\"JSON Text\"),\n  html(\"HTML\"),\n  image(\"Image\"),\n  video(\"Video\"),\n  css(\"CSS\"),\n  js(\"JavaScript\"),\n  hex(\"Hex\"),\n  ;\n\n  final String title;\n\n  const ViewType(this.title);\n\n  static ViewType? of(ContentType contentType) {\n    for (var value in values) {\n      if (value.name == contentType.name) {\n        return value;\n      }\n    }\n    return null;\n  }\n}\n\nclass HexViewer extends StatelessWidget {\n  final Uint8List data;\n  final int bytesPerRow;\n  final SearchTextController searchController;\n\n  const HexViewer({super.key, required this.data, this.bytesPerRow = 16, required this.searchController});\n\n  @override\n  Widget build(BuildContext context) {\n    return HighlightTextWidget(\n        style: const TextStyle(fontFamily: 'Courier', fontSize: 14),\n        text: _formatHex(data, bytesPerRow),\n        searchController: searchController,\n        contextMenuBuilder: contextMenu);\n  }\n\n  String _formatHex(Uint8List data, int bytesPerRow) {\n    final StringBuffer buffer = StringBuffer();\n    for (int i = 0; i < data.length; i += bytesPerRow) {\n      // Address\n      // buffer.write(i.toRadixString(16).padLeft(8, '0'));\n      // buffer.write('  ');\n\n      // Hex values\n      for (int j = 0; j < bytesPerRow; j++) {\n        if (i + j < data.length) {\n          buffer.write(data[i + j].toRadixString(16).padLeft(2, '0'));\n        } else {\n          buffer.write('  ');\n        }\n        buffer.write(' ');\n      }\n\n      buffer.write('    ');\n\n      // ASCII representation\n      for (int j = 0; j < bytesPerRow; j++) {\n        if (i + j < data.length) {\n          final byte = data[i + j];\n          if (byte >= 32 && byte <= 126) {\n            buffer.write(String.fromCharCode(byte));\n          } else {\n            buffer.write('.');\n          }\n        }\n      }\n\n      buffer.writeln();\n    }\n    return buffer.toString();\n  }\n}\n\nclass _DecodedHttpMessage extends HttpMessage {\n  final HttpMessage original;\n  final CryptoDecodedResult decoded;\n\n  _DecodedHttpMessage(this.original, this.decoded) : super(original.protocolVersion) {\n    headers.addAll(original.headers);\n    body = decoded.bytes;\n  }\n\n  @override\n  Map<String, dynamic> toJson() => original.toJson();\n\n  @override\n  String? get requestUrl => original.requestUrl;\n}\n"
  },
  {
    "path": "lib/ui/content/headers.dart",
    "content": "/*\n * Copyright 2025 Hongen Wang All rights reserved.\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 */\nimport 'package:flutter/material.dart';\nimport 'package:flutter_code_editor/flutter_code_editor.dart';\nimport 'package:flutter_highlight/themes/atom-one-dark.dart';\nimport 'package:flutter_highlight/themes/atom-one-light.dart';\nimport 'package:highlight/languages/http.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/utils/platform.dart';\n\n/// A reusable panel to display request/response headers.\n///\n/// Supports two modes:\n/// - table mode: each header shown as name/value rows\n/// - text mode: raw header lines in a read-only code field\nclass HeadersWidget extends StatefulWidget {\n  final String title;\n  final HttpMessage? message;\n  final TextStyle valueTextStyle;\n  final bool initiallyExpanded;\n\n  /// Optional shared controller for raw-text mode, so caller can reuse\n  /// controllers between rebuilds (e.g. separate for Request/Response).\n  final CodeController? controller;\n\n  const HeadersWidget({\n    super.key,\n    required this.title,\n    required this.message,\n    this.valueTextStyle = const TextStyle(fontSize: 14),\n    this.initiallyExpanded = true,\n    this.controller,\n  });\n\n  @override\n  State<HeadersWidget> createState() => _HeadersWidgetState();\n}\n\nclass _HeadersWidgetState extends State<HeadersWidget> {\n  // 静态缓存：按 title 区分的展开状态（保持同一进程内跨页面实例）\n  static final Map<String, bool> _lastExpanded = {};\n  late CodeController _controller;\n\n  // 当前实例展开状态\n  late bool _expanded;\n\n  @override\n  void initState() {\n    super.initState();\n    _controller =\n        widget.controller ?? CodeController(readOnly: true, language: http, text: _buildRawHeaders(widget.message));\n    // 优先使用按 type 缓存，其次使用全局配置，最后使用 widget 默认\n    final key = widget.title;\n    _expanded = _lastExpanded[key] ?? AppConfiguration.current?.headerExpanded ?? widget.initiallyExpanded;\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  Widget _buildHeaderModeToggle(BuildContext context) {\n\n    final config = AppConfiguration.current;\n    if (config == null) return const SizedBox();\n    final isText = config.headerViewMode == 'text';\n    void setMode(bool text) {\n      config.headerViewMode = text ? 'text' : 'table';\n      config.flushConfig();\n      setState(() {});\n    }\n\n    return IconButton(\n      visualDensity: VisualDensity.compact,\n      iconSize: 18,\n      tooltip: isText ? 'Headers: Text' : 'Headers: Table',\n      onPressed: () => setMode(!isText),\n      icon: Icon(isText ? Icons.text_snippet : Icons.table_rows),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isTextMode = (AppConfiguration.current?.headerViewMode ?? 'table') == 'text';\n    return ExpansionTile(\n      tilePadding: const EdgeInsets.only(left: 0),\n      dense: true,\n      title: Row(\n        children: [\n          Expanded(\n              child:\n                  Text('${widget.title} Headers', style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14))),\n          _buildHeaderModeToggle(context),\n        ],\n      ),\n      // 使用实例状态作为当前的展开状态\n      initiallyExpanded: _expanded,\n      onExpansionChanged: (expanded) {\n        if (_expanded == expanded) return;\n        _expanded = expanded;\n        _lastExpanded[widget.title] = expanded;\n        if (mounted) setState(() {});\n      },\n      shape: const Border(),\n      children: !isTextMode ? _buildHeaderRows(widget.message) : buildTextMode(widget.message),\n    );\n  }\n\n  List<Widget> buildTextMode(HttpMessage? message) {\n    final text = _buildRawHeaders(message);\n    if (_controller.text != text) {\n      _controller = CodeController(readOnly: true, language: http, text: text);\n    }\n\n    return [\n      CodeTheme(\n        data: CodeThemeData(\n            styles: Theme.brightnessOf(context) == Brightness.light ? atomOneLightTheme : atomOneDarkTheme),\n        child: CodeField(\n          background: Colors.transparent,\n          readOnly: Platforms.isMobile(),\n          wrap: true,\n          gutterStyle: const GutterStyle(margin: 0, width: 52, showErrors: false),\n          textStyle: const TextStyle(fontSize: 15.3),\n          controller: _controller,\n        ),\n      ),\n    ];\n  }\n\n  List<Widget> _buildHeaderRows(HttpMessage? message) {\n    final rows = <Widget>[];\n    message?.headers.forEach((name, values) {\n      for (final v in values) {\n        rows.add(Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            SelectableText(name,\n                contextMenuBuilder: contextMenu,\n                style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.deepOrangeAccent, fontSize: 15)),\n            const Text(': ',\n                style: TextStyle(fontWeight: FontWeight.w500, color: Colors.deepOrangeAccent, fontSize: 15)),\n            Expanded(\n              child: SelectableText(\n                v,\n                style: widget.valueTextStyle,\n                contextMenuBuilder: contextMenu,\n                maxLines: 8,\n                minLines: 1,\n              ),\n            ),\n          ],\n        ));\n        rows.add(const Divider(thickness: 0.1, height: 10));\n      }\n    });\n    return rows;\n  }\n\n  String _buildRawHeaders(HttpMessage? message) {\n    if (message == null) return '';\n    final buffer = StringBuffer();\n    message.headers.forEach((name, values) {\n      for (final v in values) {\n        buffer.writeln('$name: $v');\n      }\n    });\n    return buffer.toString().trimRight();\n  }\n}\n"
  },
  {
    "path": "lib/ui/content/menu.dart",
    "content": "import 'dart:convert';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/storage/favorites.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/utils/curl.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:share_plus/share_plus.dart';\n\nimport '../../utils/export_request.dart';\nimport '../../utils/python.dart';\nimport '../component/widgets.dart';\nimport '../mobile/menu/bottom_navigation.dart';\nimport '../mobile/request/request_editor.dart';\nimport '../mobile/setting/request_map.dart';\n\n///分享按钮\nclass ShareWidget extends StatelessWidget {\n  final ProxyServer? proxyServer;\n  final HttpRequest? request;\n  final HttpResponse? response;\n\n  const ShareWidget({super.key, required this.proxyServer, this.request, this.response});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    return PopupMenuButton(\n      icon: const Icon(Icons.share, size: 24),\n      offset: const Offset(0, 30),\n      itemBuilder: (BuildContext context) {\n        return [\n          PopupMenuItem(\n            padding: const EdgeInsets.only(left: 10, right: 2),\n            child: Text(localizations.shareUrl),\n            onTap: () async {\n              if (request == null) {\n                FlutterToastr.show(\"Request is empty\", context);\n                return;\n              }\n              Share.share(request!.requestUrl,\n                  subject: localizations.proxyPinSoftware, sharePositionOrigin: await _sharePositionOrigin(context));\n            },\n          ),\n          PopupMenuItem(\n              padding: const EdgeInsets.only(left: 10, right: 2),\n              child: Text(localizations.shareRequestResponse),\n              onTap: () async {\n                if (request == null) {\n                  FlutterToastr.show(\"Request is empty\", context);\n                  return;\n                }\n                var file = XFile.fromData(utf8.encode(copyRequest(request!, response)),\n                    name: localizations.captureDetail, mimeType: \"txt\");\n\n                Share.shareXFiles([file],\n                    fileNameOverrides: ['request.txt'],\n                    text: localizations.proxyPinSoftware,\n                    sharePositionOrigin: await _sharePositionOrigin(context));\n              }),\n          PopupMenuItem(\n              padding: const EdgeInsets.only(left: 10, right: 2),\n              child: Text(localizations.shareCurl),\n              onTap: () async {\n                if (request == null) {\n                  return;\n                }\n                var text = curlRequest(request!);\n                var file = XFile.fromData(utf8.encode(text), name: \"cURL.txt\", mimeType: \"txt\");\n\n                Share.shareXFiles([file],\n                    fileNameOverrides: [\"cURL.txt\"],\n                    text: localizations.proxyPinSoftware,\n                    sharePositionOrigin: await _sharePositionOrigin(context));\n              }),\n        ];\n      },\n    );\n  }\n\n  Future<Rect?> _sharePositionOrigin(BuildContext context) async {\n    RenderBox? box;\n    if (await Platforms.isIpad() && context.mounted) {\n      box = context.findRenderObject() as RenderBox?;\n    }\n    return box == null ? null : box.localToGlobal(Offset.zero) & box.size;\n  }\n}\n\nclass DetailMenuWidget extends StatelessWidget {\n  final HttpRequest? request;\n\n  const DetailMenuWidget({\n    super.key,\n    this.request,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final localizations = AppLocalizations.of(context)!;\n    return PopupMenuButton(\n        offset: const Offset(0, 30),\n        padding: const EdgeInsets.all(0),\n        itemBuilder: (context) => [\n              PopupMenuItem(\n                  child: Text(localizations.favorite),\n                  onTap: () {\n                    if (request == null) return;\n\n                    FavoriteStorage.addFavorite(request!);\n                    FlutterToastr.show(localizations.addSuccess, context);\n                  }),\n              PopupMenuItem(\n                  child: Text(localizations.save),\n                  onTap: () {\n                    if (request == null) return;\n\n                    showDialog(\n                        context: context,\n                        builder: (menuContext) {\n                          return AlertDialog(\n                              title: Text(localizations.save),\n                              content: SingleChildScrollView(\n                                child: Column(\n                                  mainAxisSize: MainAxisSize.min,\n                                  children: <Widget>[\n                                    ListTile(\n                                      visualDensity: const VisualDensity(vertical: -3),\n                                      contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),\n                                      title: Text(localizations.request),\n                                      onTap: () {\n                                        Navigator.of(menuContext).pop();\n                                        WidgetsBinding.instance.addPostFrameCallback((_) {\n                                          exportRequest(request!);\n                                        });\n                                      },\n                                    ),\n                                    ListTile(\n                                      visualDensity: const VisualDensity(vertical: -3),\n                                      contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),\n                                      title: Text(localizations.requestBody),\n                                      onTap: () {\n                                        Navigator.of(menuContext).pop();\n                                        WidgetsBinding.instance.addPostFrameCallback((_) {\n                                          exportRequestBody(request!);\n                                        });\n                                      },\n                                    ),\n                                    ListTile(\n                                      visualDensity: const VisualDensity(vertical: -3),\n                                      contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),\n                                      title: Text(localizations.response),\n                                      onTap: () {\n                                        Navigator.of(menuContext).pop();\n                                        WidgetsBinding.instance.addPostFrameCallback((_) {\n                                          exportResponse(request?.response);\n                                        });\n                                      },\n                                    ),\n                                    ListTile(\n                                      visualDensity: const VisualDensity(vertical: -3),\n                                      contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),\n                                      title: Text(localizations.responseBody),\n                                      onTap: () {\n                                        Navigator.of(menuContext).pop();\n                                        WidgetsBinding.instance.addPostFrameCallback((_) {\n                                          exportResponseBody(request?.response);\n                                        });\n                                      },\n                                    ),\n                                  ],\n                                ),\n                              ));\n                        });\n                  }),\n              PopupMenuItem(\n                  child: Text(localizations.requestEdit),\n                  onTap: () {\n                    WidgetsBinding.instance.addPostFrameCallback((_) {\n                      Navigator.of(context).push(MaterialPageRoute(\n                          builder: (context) =>\n                              MobileRequestEditor(request: request, proxyServer: ProxyServer.current)));\n                    });\n                  }),\n              PopupMenuItem(\n                  child: Text(localizations.requestMap),\n                  onTap: () {\n                    WidgetsBinding.instance.addPostFrameCallback((_) {\n                      navigator(\n                          context, MobileRequestMapEdit(url: request?.domainPath, title: request?.hostAndPort?.host));\n                    });\n                  }),\n              CustomPopupMenuItem(\n                  padding: const EdgeInsets.only(left: 10),\n                  child: Text(localizations.copyRawRequest),\n                  onTap: () {\n                    if (request == null) return;\n\n                    var text = copyRawRequest(request!);\n                    Clipboard.setData(ClipboardData(text: text));\n                    FlutterToastr.show(localizations.copied, context);\n                  }),\n              CustomPopupMenuItem(\n                  padding: const EdgeInsets.only(left: 10),\n                  child: Text(localizations.copyAsPythonRequests),\n                  onTap: () {\n                    if (request == null) return;\n\n                    var text = copyAsPythonRequests(request!);\n                    Clipboard.setData(ClipboardData(text: text));\n                    FlutterToastr.show(localizations.copied, context);\n                  })\n            ],\n        child: const SizedBox(height: 38, width: 38, child: Icon(Icons.more_vert, size: 28)));\n  }\n}\n"
  },
  {
    "path": "lib/ui/content/panel.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/ui/component/state_component.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/content/web_socket.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/platform.dart';\n\nimport 'body.dart';\nimport 'headers.dart';\nimport 'menu.dart';\n\n///网络请求详情页\n///@Author: wanghongen\nclass NetworkTabController extends StatefulWidget {\n  static GlobalKey<NetworkTabState>? currentKey;\n  final int? windowId;\n  final ProxyServer? proxyServer;\n  final ValueWrap<HttpRequest> request = ValueWrap();\n  final ValueWrap<HttpResponse> response = ValueWrap();\n  final Widget? title;\n  final TextStyle? tabStyle;\n\n  NetworkTabController(\n      {HttpRequest? httpRequest,\n      HttpResponse? httpResponse,\n      this.title,\n      this.tabStyle,\n      this.proxyServer,\n      this.windowId})\n      : super(key: GlobalKey<NetworkTabState>()) {\n    currentKey = key as GlobalKey<NetworkTabState>;\n    request.set(httpRequest);\n    response.set(httpResponse);\n  }\n\n  void change(HttpRequest? request, HttpResponse? response) {\n    this.request.set(request);\n    this.response.set(response);\n    var state = key as GlobalKey<NetworkTabState>;\n    state.currentState?.changeState();\n  }\n\n  void changeState() {\n    var state = key as GlobalKey<NetworkTabState>;\n    state.currentState?.changeState();\n  }\n\n  @override\n  State<StatefulWidget> createState() {\n    return NetworkTabState();\n  }\n\n  static NetworkTabController? get current => currentKey?.currentWidget as NetworkTabController?;\n}\n\nclass NetworkTabState extends State<NetworkTabController> with SingleTickerProviderStateMixin {\n  final tabs = [\n    'General',\n    'Request',\n    'Response',\n    'Cookies',\n  ];\n\n  final TextStyle textStyle = const TextStyle(fontSize: 14);\n  late TabController _tabController;\n\n  final GlobalKey<HttpBodyState> requestHttpBodyKey = GlobalKey<HttpBodyState>();\n  final GlobalKey<HttpBodyState> responseHttpBodyKey = GlobalKey<HttpBodyState>();\n\n  void changeState() {\n    setState(() {});\n  }\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    _tabController = TabController(length: tabs.length, vsync: this);\n    _tabController.addListener(() {\n      if (_tabController.index != 1) {\n        requestHttpBodyKey.currentState?.hideSearchOverlay();\n      }\n      if (_tabController.index != 2) {\n        responseHttpBodyKey.currentState?.hideSearchOverlay();\n      }\n    });\n\n    if (widget.windowId != null) {\n      HardwareKeyboard.instance.addHandler(onKeyEvent);\n    }\n  }\n\n  @override\n  void dispose() {\n    _tabController.dispose();\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      WindowController.fromWindowId(widget.windowId!).close();\n      return true;\n    }\n\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isWebSocket = widget.request.get()?.isWebSocket == true;\n    bool isSse = widget.response.get()?.headers.contentType.toLowerCase().startsWith('text/event-stream') == true;\n    bool isStreamMessages = isWebSocket || isSse;\n    if (isSse) {\n      tabs[tabs.length - 1] = \"SSE\";\n    } else if (isWebSocket) {\n      tabs[tabs.length - 1] = \"WebSocket\";\n    } else {\n      tabs[tabs.length - 1] = 'Cookies';\n    }\n\n    var tabBar = TabBar(\n      padding: const EdgeInsets.only(bottom: 0),\n      controller: _tabController,\n      dividerColor: Theme.of(context).dividerColor.withValues(alpha: 0.15),\n      labelPadding: const EdgeInsets.symmetric(horizontal: 10),\n      tabs: tabs.map((title) => Tab(child: Text(title, style: widget.tabStyle, maxLines: 1))).toList(),\n    );\n\n    Widget appBar = widget.title == null\n        ? tabBar\n        : AppBar(\n            title: widget.title,\n            bottom: tabBar,\n            centerTitle: true,\n            actions: [\n              ShareWidget(\n                  proxyServer: widget.proxyServer, request: widget.request.get(), response: widget.response.get()),\n              const SizedBox(width: 3),\n              DetailMenuWidget(request: widget.request.get()),\n              const SizedBox(width: 10),\n            ],\n          );\n\n    return Scaffold(\n      endDrawerEnableOpenDragGesture: false,\n      appBar: appBar as PreferredSizeWidget?,\n      body: Padding(\n          padding: const EdgeInsets.only(left: 20, right: 20, top: 10),\n          child: TabBarView(\n            physics: Platforms.isDesktop() ? const NeverScrollableScrollPhysics() : null, //桌面禁止滑动\n            controller: _tabController,\n            children: [\n              SelectionArea(child: General(widget.request, widget.response)),\n              KeepAliveWrapper(child: request()),\n              KeepAliveWrapper(child: response()),\n              SelectionArea(\n                  child: isStreamMessages\n                      ? Websocket(widget.request, widget.response)\n                      : Cookies(widget.request, widget.response)),\n            ],\n          )),\n    );\n  }\n\n  Widget request() {\n    if (widget.request.get() == null) {\n      return const SizedBox();\n    }\n\n    var scrollController = ScrollController(); //处理body也有滚动条问题\n    var path = widget.request.get()?.path ?? '';\n    try {\n      path = Uri.decodeFull(path);\n    } catch (_) {}\n\n    return SingleChildScrollView(\n        controller: scrollController,\n        child: Column(children: [\n          RowWidget(\"Path\", path),\n          RequestParams(widget.request),\n          ...message(widget.request.get(), \"Request\", scrollController)\n        ]));\n  }\n\n  Widget response() {\n    if (widget.response.get() == null) {\n      return const SizedBox();\n    }\n\n    var scrollController = ScrollController();\n    return SingleChildScrollView(\n        controller: scrollController,\n        child: Column(children: [\n          RowWidget(\"StatusCode\", widget.response.get()?.status.toString()),\n          ...message(widget.response.get(), \"Response\", scrollController)\n        ]));\n  }\n\n  List<Widget> message(HttpMessage? message, String type, ScrollController scrollController) {\n    Widget bodyWidgets = HttpBodyWidget(\n        key: type == \"Request\" ? requestHttpBodyKey : responseHttpBodyKey,\n        hideRequestRewrite: widget.windowId != null,\n        httpMessage: message,\n        scrollController: scrollController);\n\n    return [HeadersWidget(title: type, message: message, valueTextStyle: textStyle), bodyWidgets];\n  }\n}\n\nWidget expansionTile(String title, List<Widget> content,\n    {bool initiallyExpanded = true, ValueChanged<bool>? onExpansionChanged}) {\n  return ExpansionTile(\n      title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14)),\n      tilePadding: const EdgeInsets.only(left: 0),\n      expandedAlignment: Alignment.topLeft,\n      initiallyExpanded: initiallyExpanded,\n      onExpansionChanged: onExpansionChanged,\n      shape: const Border(),\n      children: content);\n}\n\nclass RequestParams extends StatelessWidget {\n  static bool initiallyExpanded = false;\n\n  final ValueWrap<HttpRequest> request;\n\n  const RequestParams(this.request, {super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    var request = this.request.get();\n    if (request == null) {\n      return const SizedBox();\n    }\n    var params = request.requestUri?.queryParametersAll;\n    if (params == null || params.isEmpty) {\n      return const SizedBox();\n    }\n    var content = <Widget>[];\n    params.forEach((name, values) {\n      for (var val in values) {\n        content.add(RowWidget(name, val));\n        content.add(const Divider(thickness: 0.1, height: 10));\n      }\n    });\n\n    return expansionTile(\"Request Params\", content, initiallyExpanded: initiallyExpanded,\n        onExpansionChanged: (expanded) {\n      //保存展开状态\n      initiallyExpanded = expanded;\n    });\n  }\n}\n\nclass General extends StatelessWidget {\n  final ValueWrap<HttpRequest> request;\n\n  final ValueWrap<HttpResponse> response;\n\n  const General(this.request, this.response, {super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    var request = this.request.get();\n    if (request == null) {\n      return const SizedBox();\n    }\n    var response = this.response.get();\n    String requestUrl = request.requestUrl;\n    try {\n      requestUrl = Uri.decodeFull(request.requestUrl);\n    } catch (_) {}\n    var content = [\n      const SizedBox(height: 10),\n      RowWidget(\"Request URL\", requestUrl),\n      const SizedBox(height: 15),\n      RowWidget(\"Request Method\", request.method.name),\n      const SizedBox(height: 15),\n      RowWidget(\"Protocol\", request.protocolVersion),\n      const SizedBox(height: 15),\n      RowWidget(\"Status Code\", response?.status.toString()),\n      const SizedBox(height: 15),\n      RowWidget(\"Remote Address\",\n          '${response?.remoteHost ?? ''}${response?.remotePort == null ? '' : ':${response?.remotePort}'}'),\n      const SizedBox(height: 15),\n      RowWidget(\"Request Time\", request.requestTime.formatMillisecond()),\n      const SizedBox(height: 15),\n      RowWidget(\"Duration\", response?.costTime()),\n      const SizedBox(height: 15),\n      RowWidget(\"Request Content-Type\", request.headers.contentType),\n      const SizedBox(height: 15),\n      RowWidget(\"Response Content-Type\", response?.headers.contentType),\n      const SizedBox(height: 15),\n      RowWidget(\"Request Package\", getPackage(request.packageSize)),\n      const SizedBox(height: 15),\n      RowWidget(\"Response Package\", getPackage(response?.packageSize)),\n      const SizedBox(height: 15),\n    ];\n    if (request.processInfo != null) {\n      content.add(RowWidget(\"App\", request.processInfo!.name));\n      content.add(const SizedBox(height: 15));\n    }\n\n    return ListView(children: [expansionTile(\"General\", content)]);\n  }\n}\n\nclass Cookies extends StatelessWidget {\n  final ValueWrap<HttpRequest> request;\n\n  final ValueWrap<HttpResponse> response;\n\n  const Cookies(this.request, this.response, {super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    var requestCookie = request.get()?.cookies.expand((cookie) => _cookieWidget(cookie)!);\n\n    var responseCookie = response.get()?.headers.getList(\"Set-Cookie\")?.expand((e) => _cookieWidget(e)!);\n    return ListView(children: [\n      requestCookie == null ? const SizedBox() : expansionTile(\"Request Cookies\", requestCookie.toList()),\n      const SizedBox(height: 15),\n      responseCookie == null ? const SizedBox() : expansionTile(\"Response Cookies\", responseCookie.toList()),\n    ]);\n  }\n\n  Iterable<Widget>? _cookieWidget(String? cookie) {\n    var headers = <Widget>[];\n\n    cookie?.split(\";\").map((e) => Strings.splitFirst(e, \"=\")).where((element) => element != null).forEach((e) {\n      headers.add(RowWidget(e!.key.trim(), e.value));\n      headers.add(const Divider(thickness: 0.1, height: 10));\n    });\n\n    return headers;\n  }\n}\n\nclass RowWidget extends StatelessWidget {\n  final String name;\n  final String? value;\n  final TextStyle textStyle = const TextStyle(fontSize: 14);\n\n  const RowWidget(this.name, this.value, {super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(children: [\n      Expanded(\n          flex: 2,\n          child: SelectableText(name,\n              contextMenuBuilder: contextMenu,\n              style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14, color: Colors.deepOrangeAccent))),\n      Expanded(flex: 4, child: SelectableText(contextMenuBuilder: contextMenu, style: textStyle, value ?? ''))\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/content/web_socket.dart",
    "content": "import 'dart:convert';\nimport 'dart:math';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/num.dart';\n\nimport '../../l10n/app_localizations.dart';\nimport '../../network/http/http.dart';\nimport '../../network/http/websocket.dart';\nimport '../../utils/platform.dart';\nimport '../component/app_dialog.dart';\nimport '../component/json/json_text.dart';\nimport '../component/json/json_viewer.dart';\nimport '../component/json/theme.dart';\n\n///以聊天对话框样式展示websocket消息\nclass Websocket extends StatelessWidget {\n  final ValueWrap<HttpRequest> request;\n  final ValueWrap<HttpResponse> response;\n\n  const Websocket(this.request, this.response, {super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    var request = this.request.get();\n    if (request == null) {\n      return const SizedBox();\n    }\n    List<WebSocketFrame> messages = List.from(request.messages);\n    var response = this.response.get();\n    if (response != null) {\n      messages.addAll(response.messages);\n    }\n    messages.sort((a, b) => a.time.compareTo(b.time));\n\n    return ListView.builder(\n      padding: const EdgeInsets.only(bottom: 15),\n      itemCount: messages.length,\n      itemBuilder: (context, index) {\n        var message = messages[index];\n        var avatar = SelectionContainer.disabled(\n            child: CircleAvatar(\n                backgroundColor: message.isFromClient ? Colors.green : Colors.blue,\n                child:\n                    Text(message.isFromClient ? 'C' : 'S', style: const TextStyle(fontSize: 18, color: Colors.white))));\n\n        var previewButton = IconButton(\n          tooltip: \"Preview\",\n          onPressed: () {\n            showDialog(context: context, builder: (context) => _PreviewDialog(bytes: message.payloadData));\n          },\n          icon: Icon(Icons.expand_more, color: ColorScheme.of(context).primary),\n        );\n\n        return Padding(\n            padding: const EdgeInsets.only(bottom: 5),\n            child: Row(\n              mainAxisAlignment: message.isFromClient ? MainAxisAlignment.start : MainAxisAlignment.end,\n              children: [\n                if (message.isFromClient) avatar,\n                const SizedBox(width: 8),\n                Flexible(\n                  child: Column(\n                      crossAxisAlignment: message.isFromClient ? CrossAxisAlignment.start : CrossAxisAlignment.end,\n                      children: [\n                        SelectionContainer.disabled(\n                            child:\n                                Text(message.time.format(), style: const TextStyle(fontSize: 12, color: Colors.grey))),\n                        Row(mainAxisSize: MainAxisSize.min, children: [\n                          if (!message.isFromClient) previewButton,\n                          Flexible(\n                            child: Container(\n                                padding: const EdgeInsets.all(8),\n                                decoration: BoxDecoration(\n                                  color: message.isFromClient\n                                      ? Colors.green.withOpacity(0.26)\n                                      : Colors.blue.withOpacity(0.3),\n                                  borderRadius: BorderRadius.circular(10),\n                                ),\n                                child: SelectableText(\n                                  \"${message.payloadDataAsString}${message.isBinary ? ' ${getPackage(message.payloadLength)}' : ''}\",\n                                  maxLines: 3,\n                                  minLines: 1,\n                                  contextMenuBuilder: (context, editableTextState) =>\n                                      contextMenu(context, editableTextState,\n                                          customItem: ContextMenuButtonItem(\n                                            label: localizations.download,\n                                            onPressed: () async {\n                                              String? path = (await FilePicker.platform\n                                                  .saveFile(fileName: \"websocket.txt\", bytes: message.payloadData));\n                                              if (path != null && context.mounted) {\n                                                CustomToast.success(localizations.saveSuccess).show(context);\n                                              }\n                                            },\n                                            type: ContextMenuButtonType.custom,\n                                          )),\n                                )),\n                          ),\n                          if (message.isFromClient) previewButton,\n                        ])\n                      ]),\n                ),\n                const SizedBox(width: 8),\n                if (!message.isFromClient) avatar,\n              ],\n            ));\n      },\n    );\n  }\n}\n\nclass _PreviewDialog extends StatefulWidget {\n  final List<int> bytes;\n\n  const _PreviewDialog({required this.bytes});\n\n  @override\n  State<_PreviewDialog> createState() => _PreviewDialogState();\n}\n\nclass _PreviewDialogState extends State<_PreviewDialog> {\n  int tabIndex = 0; // 0: HEX, 1: TEXT\n\n  @override\n  Widget build(BuildContext context) {\n    var tabs = [\n      if (isJsonText(widget.bytes)) const Tab(text: \"JSON Text\"),\n      if (isJsonText(widget.bytes)) const Tab(text: \"JSON\"),\n      const Tab(text: \"TEXT\"),\n      const Tab(text: \"HEX\"),\n    ];\n\n    return AlertDialog(\n      content: SizedBox(\n        width: min(MediaQuery.of(context).size.width * 0.8, 700),\n        height: min(MediaQuery.of(context).size.height * 0.6, 650),\n        child: DefaultTabController(\n          length: tabs.length,\n          initialIndex: tabIndex,\n          child: Column(mainAxisSize: MainAxisSize.min, children: [\n            TabBar(\n              tabs: tabs,\n              onTap: (index) {\n                setState(() {\n                  tabIndex = index;\n                });\n              },\n            ),\n            Expanded(\n              child: TabBarView(children: [\n                if (isJsonText(widget.bytes))\n                  SingleChildScrollView(padding: const EdgeInsets.all(8.0), child: jsonText()),\n                if (isJsonText(widget.bytes))\n                  SingleChildScrollView(padding: const EdgeInsets.all(8.0), child: jsonView()),\n                // TEXT\n                SingleChildScrollView(\n                  padding: const EdgeInsets.all(8),\n                  child: SelectableText(safeTextPreview(widget.bytes)),\n                ),\n\n                // HEX\n                SingleChildScrollView(\n                  padding: const EdgeInsets.all(8),\n                  child: SelectableText(widget.bytes.map(intToHex).join(\" \")),\n                ),\n              ]),\n            ),\n          ]),\n        ),\n      ),\n      actions: [\n        TextButton(\n            onPressed: () => Navigator.of(context).pop(),\n            child: Text(MaterialLocalizations.of(context).closeButtonLabel))\n      ],\n    );\n  }\n\n  Widget jsonText() {\n    String body = utf8.decode(widget.bytes, allowMalformed: true);\n    dynamic jsonData;\n    try {\n      jsonData = json.decode(body);\n    } catch (e) {\n      jsonData = null;\n    }\n\n    if (jsonData == null) {\n      return SelectableText(safeTextPreview(widget.bytes));\n    }\n\n    return JsonText(json: jsonData, indent: Platforms.isDesktop() ? '    ' : '  ', colorTheme: ColorTheme.of(context));\n  }\n\n  Widget jsonView() {\n    String body = utf8.decode(widget.bytes, allowMalformed: true);\n    dynamic jsonData;\n    try {\n      jsonData = json.decode(body);\n    } catch (e) {\n      jsonData = null;\n    }\n\n    if (jsonData == null) {\n      return SelectableText(safeTextPreview(widget.bytes));\n    }\n\n    return JsonViewer(json.decode(body), colorTheme: ColorTheme.of(context));\n  }\n\n  //判断是否是json格式\n  bool isJsonText(List<int> bytes) {\n    return bytes.isNotEmpty && (bytes[0] == 0x7B || bytes[0] == 0x5B);\n  }\n\n  /// Format bytes as hex-dump string: 4 bytes per group (8 hex digits), space between groups, line break every 16 bytes\n  String formatHexDump(List<int> bytes) {\n    final buffer = StringBuffer();\n\n    // 每8个字节为一组，用空格分隔，组之间换行\n    for (int i = 0; i < bytes.length; i++) {\n      // 添加当前十六进制部分\n      buffer.write(bytes[i].toRadixString(16).padLeft(2, '0'));\n      if ((i + 1) % 2 == 0 && i != bytes.length - 1) {\n        buffer.write(' ');\n      }\n    }\n    return buffer.toString();\n  }\n\n  /// Decode bytes to string, non-printable as '.'\n  String safeTextPreview(List<int> bytes) {\n    try {\n      return utf8.decode(bytes);\n    } catch (_) {\n      return bytes.map((b) => b >= 32 && b <= 126 ? String.fromCharCode(b) : '.').join();\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/common.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/network/http/http.dart';\n\nimport 'setting/request_rewrite.dart';\n\n/// 显示请求重写对话框\nFuture<void> showRequestRewriteDialog(BuildContext context, HttpRequest request) async {\n  bool isRequest = request.response == null;\n  var requestRewrites = await RequestRewriteManager.instance;\n\n  var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace;\n  var rule = requestRewrites.getRequestRewriteRule(request, ruleType);\n  var rewriteItems = await requestRewrites.getRewriteItems(rule);\n  if (!context.mounted) return;\n\n  showDialog(\n      context: context,\n      barrierDismissible: false,\n      builder: (context) => RewriteRuleEdit(rule: rule, items: rewriteItems, request: request));\n}\n"
  },
  {
    "path": "lib/ui/desktop/debug/breakpoint_executor.dart",
    "content": "import 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/ui/desktop/request/request_editor.dart';\n\nclass BreakpointExecutor extends StatefulWidget {\n  final int? windowId;\n  final HttpRequest request;\n  final HttpResponse? response;\n  final String requestId;\n\n  // false: intercept request, true: intercept response\n  final bool isResponse;\n\n  const BreakpointExecutor({\n    super.key,\n    required this.request,\n    this.response,\n    required this.requestId,\n    required this.isResponse,\n    this.windowId,\n  });\n\n  @override\n  State<BreakpointExecutor> createState() => _BreakpointExecutorState();\n}\n\nclass _BreakpointExecutorState extends State<BreakpointExecutor> {\n  late HttpRequest request;\n  late HttpResponse? response;\n\n  @override\n  void initState() {\n    super.initState();\n    request = widget.request;\n    response = widget.response;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.isResponse) {\n      return _buildResponseBody();\n    }\n\n    return RequestEditor(\n      request: request,\n      source: RequestEditorSource.breakpointRequest,\n      onExecuteRequest: (newRequest) async {\n        await DesktopMultiWindow.invokeMethod(0, 'resumeRequest', {\n          'requestId': widget.requestId,\n          'request': newRequest?.toJson(),\n        });\n        if (widget.windowId != null) {\n          WindowController.fromWindowId(widget.windowId!).close();\n        }\n      },\n    );\n  }\n\n  Widget _buildResponseBody() {\n    return RequestEditor(\n      request: request,\n      response: response,\n      source: RequestEditorSource.breakpointResponse,\n      onExecuteResponse: (newResponse) async {\n        await DesktopMultiWindow.invokeMethod(0, 'resumeResponse', {\n          'requestId': widget.requestId,\n          'response': newResponse?.toJson(),\n        });\n        if (widget.windowId != null) {\n          WindowController.fromWindowId(widget.windowId!).close();\n        }\n      },\n    );\n  }\n\n}\n"
  },
  {
    "path": "lib/ui/desktop/desktop.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/bin/listener.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/websocket.dart';\nimport 'package:proxypin/ui/component/memory_cleanup.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/content/panel.dart';\nimport 'package:proxypin/ui/desktop/left_menus/favorite.dart';\nimport 'package:proxypin/ui/desktop/left_menus/history.dart';\nimport 'package:proxypin/ui/desktop/left_menus/navigation.dart';\nimport 'package:proxypin/ui/desktop/request/list.dart';\nimport 'package:proxypin/ui/desktop/toolbar/toolbar.dart';\nimport 'package:proxypin/ui/desktop/widgets/windows_toolbar.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\n\nimport '../app_update/app_update_repository.dart';\nimport '../component/split_view.dart';\nimport '../toolbox/toolbox.dart';\n\n/// @author wanghongen\n/// 2023/10/8\nclass DesktopHomePage extends StatefulWidget {\n  final Configuration configuration;\n  final AppConfiguration appConfiguration;\n\n  const DesktopHomePage(this.configuration, this.appConfiguration, {super.key, required});\n\n  @override\n  State<DesktopHomePage> createState() => _DesktopHomePagePageState();\n}\n\nclass _DesktopHomePagePageState extends State<DesktopHomePage> implements EventListener {\n  static final container = ListenableList<HttpRequest>();\n\n  static final GlobalKey<DesktopRequestListState> requestListStateKey = GlobalKey<DesktopRequestListState>();\n\n  final ValueNotifier<int> _selectIndex = ValueNotifier(0);\n\n  late ProxyServer proxyServer = ProxyServer(widget.configuration);\n  late NetworkTabController panel;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void onRequest(Channel channel, HttpRequest request) {\n    requestListStateKey.currentState!.add(channel, request);\n\n    //监控内存 到达阈值清理\n    MemoryCleanupMonitor.onMonitor(onCleanup: () {\n      requestListStateKey.currentState?.cleanupEarlyData(32);\n    });\n  }\n\n  @override\n  void onResponse(ChannelContext channelContext, HttpResponse response) {\n    requestListStateKey.currentState!.addResponse(channelContext, response);\n  }\n\n  @override\n  void onMessage(Channel channel, HttpMessage message, WebSocketFrame frame) {\n    if (panel.request.get() == message || panel.response.get() == message) {\n      panel.changeState();\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    proxyServer.addListener(this);\n    panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 16), proxyServer: proxyServer);\n\n    if (widget.appConfiguration.upgradeNoticeV26) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        showUpgradeNotice();\n      });\n    } else {\n      AppUpdateRepository.checkUpdate(context);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var navigationView = [\n      DesktopRequestListWidget(key: requestListStateKey, proxyServer: proxyServer, list: container, panel: panel),\n      Favorites(panel: panel),\n      HistoryPageWidget(proxyServer: proxyServer, container: container, panel: panel),\n      const Toolbox()\n    ];\n\n    return Scaffold(\n        appBar: Tab(\n            child: Container(\n          padding: EdgeInsets.only(bottom: 2.5),\n          margin: EdgeInsets.only(bottom: 5),\n          decoration: BoxDecoration(\n              // color: Theme.of(context).brightness == Brightness.dark ? null : Color(0xFFF9F9F9),\n              border: Border(\n                  bottom: BorderSide(\n                      color: Theme.of(context).dividerColor.withValues(alpha: 0.3),\n                      width: Platform.isMacOS ? 0.2 : 0.55))),\n          child: Platform.isMacOS\n              ? Toolbar(proxyServer, requestListStateKey)\n              : WindowsToolbar(title: Toolbar(proxyServer, requestListStateKey)),\n        )),\n        body: Row(\n          children: [\n            LeftNavigationBar(\n                selectIndex: _selectIndex, appConfiguration: widget.appConfiguration, proxyServer: proxyServer),\n            Expanded(\n              child: VerticalSplitView(\n                  ratio: widget.appConfiguration.panelRatio,\n                  minRatio: 0.15,\n                  maxRatio: 0.9,\n                  onRatioChanged: (ratio) {\n                    widget.appConfiguration.panelRatio = double.parse(ratio.toStringAsFixed(2));\n                    widget.appConfiguration.flushConfig();\n                  },\n                  left: ValueListenableBuilder(\n                      valueListenable: _selectIndex,\n                      builder: (_, index, __) =>\n                          LazyIndexedStack(index: index < 0 ? 0 : index, children: navigationView)),\n                  right: panel),\n            )\n          ],\n        ));\n  }\n\n  //更新引导\n  void showUpgradeNotice() {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (_) {\n          return AlertDialog(\n              scrollable: true,\n              actions: [\n                TextButton(\n                    onPressed: () {\n                      widget.appConfiguration.upgradeNoticeV26 = false;\n                      widget.appConfiguration.flushConfig();\n                      Navigator.pop(context);\n                    },\n                    child: Text(localizations.close))\n              ],\n              title: Text(isCN ? '更新内容V${AppConfiguration.version}' : \"What's new in V${AppConfiguration.version}\",\n                  style: const TextStyle(fontSize: 18)),\n              content: Container(\n                  constraints: const BoxConstraints(maxWidth: 600),\n                  child: SelectableText(\n                      isCN\n                          ? '提示：默认不会开启HTTPS抓包，请安装证书后再开启HTTPS抓包。\\n'\n                              '点击HTTPS抓包(加锁图标)，选择安装根证书，按照提示操作即可。\\n\\n'\n                              '1. 新增请求断点，可修改请求、响应后发送；\\n'\n                              '2. 在请求编辑器中为Header添加自动补全建议；\\n'\n                              '3. Android、iOS新增系统代理IP忽略设置；\\n'\n                              '4. Android新增系统代理是否启用设置；\\n'\n                              '5. Socks5代理新增 IPV6 支持；\\n'\n                              '6. 修复 MacOS 网线代理设置失败问题；\\n'\n                          : 'Note: HTTPS capture is disabled by default — please install the certificate before enabling HTTPS capture.\\n'\n                              'Click the HTTPS capture (lock) icon, choose \"Install Root Certificate\", and follow the prompts to complete installation.\\n\\n'\n                              '1. Added request breakpoint feature, allowing modification of requests and responses before sending;\\n'\n                              '2. Added autocomplete suggestions for HTTP headers in request editor;\\n'\n                              '3. Added system proxy IP ignore settings for Android and iOS;\\n'\n                              '4. Added system proxy enable/disable settings for Android;\\n'\n                              '5. Added IPv6 support for Socks5 proxy;\\n'\n                              '6. Fixed an issue where proxy settings failed on macOS; \\n'\n                              ,\n                      style: const TextStyle(fontSize: 14))));\n        });\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/left_menus/favorite.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:collection';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:date_format/date_format.dart';\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/storage/favorites.dart';\nimport 'package:proxypin/ui/component/app_dialog.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/content/panel.dart';\nimport 'package:proxypin/ui/desktop/request/repeat.dart';\nimport 'package:proxypin/utils/curl.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/python.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\nimport 'package:window_manager/window_manager.dart';\n\nimport '../common.dart';\n\n/// @author wanghongen\n/// 2023/10/8\nclass Favorites extends StatefulWidget {\n  final NetworkTabController panel;\n\n  const Favorites({super.key, required this.panel});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _FavoritesState();\n  }\n}\n\nclass _FavoritesState extends State<Favorites> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    FavoriteStorage.addNotifier = () {\n      setState(() {});\n    };\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n        future: FavoriteStorage.favorites,\n        builder: (BuildContext context, AsyncSnapshot<Queue<Favorite>> snapshot) {\n          if (snapshot.hasData) {\n            var favorites = snapshot.data ?? Queue();\n            if (favorites.isEmpty) {\n              return Center(child: Text(localizations.emptyFavorite));\n            }\n\n            return ListView.separated(\n              itemCount: favorites.length + 1,\n              itemBuilder: (_, index) {\n                if (index == 0) {\n                  return _FavoritesActions(onChanged: () => setState(() {}));\n                }\n                var request = favorites.elementAt(index - 1);\n                return _FavoriteItem(\n                  request,\n                  index: index - 1,\n                  panel: widget.panel,\n                  onRemove: (Favorite favorite) {\n                    FavoriteStorage.removeFavorite(favorite);\n                    CustomToast.success(localizations.deleteFavoriteSuccess).show(context);\n                    setState(() {});\n                  },\n                );\n              },\n              separatorBuilder: (_, idx) =>\n                  idx == 0 ? const SizedBox(height: 4) : const Divider(height: 1, thickness: 0.3),\n            );\n          } else {\n            return const SizedBox();\n          }\n        });\n  }\n}\n\nclass _FavoriteItem extends StatefulWidget {\n  final int index;\n  final Favorite favorite;\n  final NetworkTabController panel;\n  final Function(Favorite favorite)? onRemove;\n\n  const _FavoriteItem(this.favorite, {required this.panel, required this.onRemove, required this.index});\n\n  @override\n  State<_FavoriteItem> createState() => _FavoriteItemState();\n}\n\nclass _FavoriteItemState extends State<_FavoriteItem> {\n  //选择的节点\n  static _FavoriteItemState? selectedState;\n\n  bool selected = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    var request = widget.favorite.request;\n\n    var response = widget.favorite.response;\n    var title = '${request.method.name} ${request.requestUrl}'.fixAutoLines();\n    var time = formatDate(request.requestTime, [mm, '-', d, ' ', HH, ':', nn, ':', ss]);\n\n    return GestureDetector(\n        onSecondaryLongPressDown: (details) => menu(details, request),\n        child: ListTile(\n            minLeadingWidth: 25,\n            leading: getIcon(response),\n            title: Text(widget.favorite.name ?? title, overflow: TextOverflow.ellipsis, maxLines: 2),\n            trailing: request.isWebSocket\n                ? Text(\n                    'WS',\n                    style: TextStyle(\n                      fontSize: 10,\n                      fontWeight: FontWeight.w600,\n                      color: Theme.of(context).colorScheme.primary,\n                    ),\n                  )\n                : null,\n            subtitle: Text.rich(\n                style: const TextStyle(fontSize: 12),\n                maxLines: 1,\n                TextSpan(children: [\n                  TextSpan(text: '#${widget.index} ', style: const TextStyle(color: Colors.teal)),\n                  TextSpan(\n                      text:\n                          '$time - [${response?.status.code ?? ''}]  ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''} '),\n                ])),\n            selected: selected,\n            dense: true,\n            onTap: () => onClick(request)));\n  }\n\n  ///右键菜单\n  void menu(LongPressDownDetails details, HttpRequest request) {\n    showContextMenu(\n      context,\n      details.globalPosition,\n      items: <PopupMenuEntry>[\n        popupItem(localizations.copyUrl, onTap: () {\n          var requestUrl = request.requestUrl;\n          Clipboard.setData(ClipboardData(text: requestUrl))\n              .then((value) => FlutterToastr.show(localizations.copied, context));\n        }),\n        popupItem(localizations.copyRequestResponse, onTap: () {\n          Clipboard.setData(ClipboardData(text: copyRequest(request, request.response)))\n              .then((value) => FlutterToastr.show(localizations.copied, context));\n        }),\n        popupItem(localizations.copyCurl, onTap: () {\n          Clipboard.setData(ClipboardData(text: curlRequest(request)))\n              .then((value) => FlutterToastr.show(localizations.copied, context));\n        }),\n        popupItem(localizations.copyAsPythonRequests, onTap: () {\n          Clipboard.setData(ClipboardData(text: copyAsPythonRequests(request)))\n              .then((value) => FlutterToastr.show(localizations.copied, context));\n        }),\n        const PopupMenuDivider(height: 0.3),\n        popupItem(localizations.repeat, onTap: () => onRepeat(request)),\n        popupItem(localizations.customRepeat, onTap: () => showCustomRepeat(request)),\n        popupItem(localizations.editRequest, onTap: () {\n          WidgetsBinding.instance.addPostFrameCallback((_) {\n            requestEdit(request);\n          });\n        }),\n        popupItem(localizations.requestRewrite, onTap: () => showRequestRewriteDialog(context, request)),\n        const PopupMenuDivider(height: 0.3),\n        popupItem(localizations.rename, onTap: () => rename(widget.favorite)),\n        popupItem(localizations.deleteFavorite, onTap: () {\n          widget.onRemove?.call(widget.favorite);\n        })\n      ],\n    );\n  }\n\n  //显示高级重发\n  Future<void> showCustomRepeat(HttpRequest request) async {\n    var prefs = await SharedPreferences.getInstance();\n    if (!mounted) return;\n\n    showDialog(\n        context: context,\n        builder: (BuildContext context) {\n          return CustomRepeatDialog(onRepeat: () => onRepeat(request), prefs: prefs);\n        });\n  }\n\n  void onRepeat(HttpRequest request) {\n    var httpRequest = request.copy(uri: request.requestUrl);\n    if (widget.panel.proxyServer == null) {\n      return;\n    }\n\n    var proxyInfo =\n        widget.panel.proxyServer!.isRunning ? ProxyInfo.of(\"127.0.0.1\", widget.panel.proxyServer!.port) : null;\n    HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo);\n\n    if (mounted) {\n      FlutterToastr.show(localizations.reSendRequest, context);\n    }\n  }\n\n  PopupMenuItem popupItem(String text, {VoidCallback? onTap}) {\n    return CustomPopupMenuItem(height: 35, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 13)));\n  }\n\n  //重命名\n  void rename(Favorite item) {\n    String? name = item.name;\n    showDialog(\n        context: context,\n        builder: (context) {\n          return AlertDialog(\n            content: TextFormField(\n              initialValue: name,\n              decoration: InputDecoration(label: Text(localizations.name)),\n              onChanged: (val) => name = val,\n            ),\n            actions: <Widget>[\n              TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n              TextButton(\n                child: Text(localizations.save),\n                onPressed: () {\n                  Navigator.maybePop(context);\n                  setState(() {\n                    item.name = name?.isEmpty == true ? null : name;\n                    FavoriteStorage.flushConfig();\n                  });\n                },\n              ),\n            ],\n          );\n        });\n  }\n\n  ///请求编辑\n  Future<void> requestEdit(HttpRequest request) async {\n    var size = MediaQuery.of(context).size;\n    var ratio = 1.0;\n    if (Platform.isWindows) {\n      ratio = WindowManager.instance.getDevicePixelRatio();\n    }\n\n    final window = await DesktopMultiWindow.createWindow(jsonEncode(\n      {'name': 'RequestEditor', 'request': request},\n    ));\n    window.setTitle(localizations.requestEdit);\n    window\n      ..setFrame(const Offset(100, 100) & Size(960 * ratio, size.height * ratio))\n      ..center()\n      ..show();\n  }\n\n  //点击事件\n  void onClick(HttpRequest request) {\n    if (selected) {\n      return;\n    }\n    setState(() {\n      selected = true;\n    });\n\n    //切换选中的节点\n    if (selectedState?.mounted == true && selectedState != this) {\n      selectedState?.setState(() {\n        selectedState?.selected = false;\n      });\n    }\n    selectedState = this;\n    widget.panel.change(request, request.response);\n  }\n}\n\nclass _FavoritesActions extends StatelessWidget {\n  final VoidCallback onChanged;\n\n  const _FavoritesActions({required this.onChanged});\n\n  @override\n  Widget build(BuildContext context) {\n    final localizations = AppLocalizations.of(context)!;\n\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        SizedBox(\n          height: 36,\n          child: Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 8),\n            child: Row(\n              children: [\n                Text(\n                  localizations.favorites,\n                  style: TextStyle(\n                    fontSize: 12.5,\n                    color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.82),\n                  ),\n                ),\n                const Spacer(),\n                // IconButton(\n                //   tooltip: '${localizations.export} HAR',\n                //   padding: const EdgeInsets.symmetric(horizontal: 6),\n                //   constraints: const BoxConstraints(minWidth: 34, minHeight: 34),\n                //   icon: const Icon(Icons.upload, size: 18),\n                //   onPressed: () async {\n                //     final path = await FilePicker.platform.saveFile(fileName: 'favorites.har');\n                //     if (path == null) return;\n                //     await FavoriteStorage.exportToHarFile(path, title: localizations.favorites);\n                //     FlutterToastr.show(localizations.exportSuccess, context);\n                //   },\n                // ),\n                IconButton(\n                  tooltip: localizations.export,\n                  padding: const EdgeInsets.symmetric(horizontal: 6),\n                  constraints: const BoxConstraints(minWidth: 34, minHeight: 34),\n                  icon: const Icon(Icons.upload_file, size: 18),\n                  onPressed: () async {\n                    final path = await FilePicker.platform.saveFile(fileName: 'favorites.json');\n                    if (path == null) return;\n                    await FavoriteStorage.exportToFile(path);\n                    if (context.mounted) CustomToast.success(localizations.exportSuccess).show(context);\n                    onChanged();\n                  },\n                ),\n                const SizedBox(width: 3),\n                IconButton(\n                  tooltip: localizations.import,\n                  constraints: const BoxConstraints(minWidth: 34, minHeight: 34),\n                  icon: const Icon(Icons.download_for_offline_outlined, size: 18),\n                  onPressed: () async {\n                    final result =\n                        await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json', 'har']);\n                    final file = result?.files.isNotEmpty == true ? result!.files.first : null;\n                    if (file?.path == null) return;\n\n                    try {\n                      await FavoriteStorage.importFromFile(file!.path!);\n                      if (context.mounted) CustomToast.success(localizations.importSuccess).show(context);\n                      onChanged();\n                    } catch (e) {\n                      logger.e('Import favorites failed: $e');\n                      if (context.mounted) CustomToast.error('${localizations.importFailed}: $e').show(context);\n                    }\n                  },\n                ),\n              ],\n            ),\n          ),\n        ),\n        const Divider(height: 1, thickness: 0.4),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/left_menus/history.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:async';\nimport 'dart:io';\nimport 'dart:math';\n\nimport 'package:date_format/date_format.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/storage/histories.dart';\nimport 'package:proxypin/ui/component/history_cache_time.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/utils/har.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\n\nimport '../../content/panel.dart';\nimport '../request/list.dart';\n\n/// 历史记录\n/// @author wanghongen\n/// 2023/10/8\nclass HistoryPageWidget extends StatelessWidget {\n  final ProxyServer proxyServer;\n  final ListenableList<HttpRequest> container;\n  final NetworkTabController panel;\n  final HistoryTask historyTask;\n\n  HistoryPageWidget({super.key, required this.proxyServer, required this.container, required this.panel})\n      : historyTask = HistoryTask.ensureInstance(proxyServer.configuration, container);\n\n  @override\n  Widget build(BuildContext context) {\n    return Navigator(\n      onGenerateRoute: (settings) {\n        switch (settings.name) {\n          case \"/request_list\":\n            return MaterialPageRoute(builder: (_) => requestListWidget(context, settings.arguments as Map));\n          default:\n            return MaterialPageRoute(\n                builder: (_) => futureWidget(\n                      HistoryStorage.instance,\n                      (storage) => _HistoryListWidget(storage,\n                          container: container, proxyServer: proxyServer, historyTask: historyTask),\n                    ));\n        }\n      },\n    );\n  }\n\n  Widget requestListWidget(BuildContext context, Map arguments) {\n    var requestListKey = GlobalKey<DesktopRequestListState>();\n\n    HistoryItem item = arguments['item'];\n    var localizations = AppLocalizations.of(context)!;\n\n    return Scaffold(\n        appBar: PreferredSize(\n            preferredSize: const Size.fromHeight(40),\n            child: AppBar(\n              leadingWidth: 50,\n              leading: BackButton(style: ButtonStyle(iconSize: WidgetStateProperty.all(15))),\n              centerTitle: false,\n              title: Text(\n                  textAlign: TextAlign.start,\n                  localizations.historyRecordTitle(\n                      item.requestLength, item.name.substring(0, min(item.name.length, 25))),\n                  style: const TextStyle(fontSize: 14)),\n            )),\n        body: futureWidget(HistoryStorage.instance.then((value) => value.getRequests(item)), (data) {\n          //shrinkWrap: false,\n          return DesktopRequestListWidget(\n              panel: panel, proxyServer: proxyServer, list: ListenableList(data), key: requestListKey);\n        }, loading: true));\n  }\n}\n\n///历史记录列表\nclass _HistoryListWidget extends StatefulWidget {\n  // 存储\n  final HistoryStorage storage;\n  final ListenableList<HttpRequest> container;\n  final ProxyServer proxyServer;\n  final HistoryTask historyTask;\n\n  const _HistoryListWidget(this.storage,\n      {required this.container, required this.proxyServer, required this.historyTask});\n\n  @override\n  State<StatefulWidget> createState() => _HistoryListState();\n}\n\nclass _HistoryListState extends State<_HistoryListWidget> {\n  ///是否保存会话\n  static bool _sessionSaved = false;\n  int selectIndex = -1;\n\n  // 存储\n  late HistoryStorage storage;\n\n  late ListenableList<HttpRequest> container;\n  late ProxyServer proxyServer;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    storage = widget.storage;\n    container = widget.container;\n    proxyServer = widget.proxyServer;\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      storage.addListener(OnchangeListEvent(() {\n        if (mounted) setState(() {});\n      }));\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    List<Widget> children = [];\n    if (!_sessionSaved && proxyServer.configuration.historyCacheTime == 0 && widget.historyTask.history == null) {\n      //当前会话未保存，是否保存当前会话\n      children.add(buildSaveSession());\n    }\n\n    var histories = storage.histories;\n    for (int i = histories.length - 1; i >= 0; i--) {\n      var entry = histories.elementAt(i);\n      children.add(buildItem(context, i, entry));\n    }\n\n    return Scaffold(\n        appBar: PreferredSize(\n            preferredSize: const Size.fromHeight(36),\n            child: AppBar(\n              toolbarHeight: 36,\n              titleSpacing: 8,\n              centerTitle: false,\n              title: Text(\n                localizations.historyRecord,\n                style: TextStyle(\n                  fontSize: 12.5,\n                  color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.82),\n                ),\n              ),\n              bottom: const PreferredSize(preferredSize: Size.fromHeight(1), child: Divider(height: 1, thickness: 0.4)),\n              actions: [\n                IconButton(\n                    onPressed: import,\n                    icon: const Icon(Icons.input, size: 18),\n                    constraints: const BoxConstraints(minWidth: 34, minHeight: 34),\n                    tooltip: localizations.import),\n                const SizedBox(width: 3),\n                HistoryCacheTime(proxyServer.configuration, onSelected: (val) {\n                  if (val == 0) {\n                    widget.container.removeListener(widget.historyTask);\n                  } else {\n                    widget.container.addListener(widget.historyTask);\n                  }\n                }),\n                const SizedBox(width: 5)\n              ],\n            )),\n        body: ListView.separated(\n          itemCount: children.length,\n          itemBuilder: (_, index) => children[index],\n          separatorBuilder: (_, index) => const Divider(thickness: 0.3, height: 0),\n        ));\n  }\n\n  //导入har\n  Future<void> import() async {\n    final results = await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['har']);\n    if (results == null || results.files.isEmpty) {\n      return;\n    }\n\n    var file = results.files.first;\n    try {\n      var historyItem = await storage.addHarFile(file.xFile);\n      setState(() {\n        toRequestsView(historyItem);\n        FlutterToastr.show(localizations.importSuccess, context);\n      });\n    } catch (e, t) {\n      logger.e('导入失败 $file', error: e, stackTrace: t);\n      if (mounted) {\n        FlutterToastr.show(\"${localizations.importFailed} $e\", context);\n      }\n    }\n  }\n\n  //构建保存会话\n  Widget buildSaveSession() {\n    var name = formatDate(DateTime.now(), [mm, '-', d, ' ', HH, ':', nn, ':', ss]);\n\n    return ListTile(\n        dense: true,\n        title: Text(name),\n        subtitle: Text(localizations.historyUnSave),\n        trailing: TextButton.icon(\n          icon: const Icon(Icons.save),\n          label: Text(localizations.save),\n          onPressed: () async {\n            widget.container.addListener(widget.historyTask);\n            widget.historyTask.startTask();\n\n            setState(() {\n              _sessionSaved = true;\n            });\n          },\n        ),\n        onTap: () {});\n  }\n\n  //构建历史记录\n  Widget buildItem(BuildContext rootContext, int index, HistoryItem item) {\n    return GestureDetector(\n        onSecondaryTapDown: (details) {\n          setState(() {\n            selectIndex = index;\n          });\n          showContextMenu(rootContext, details.globalPosition, items: [\n            CustomPopupMenuItem(\n                height: 35,\n                child: Text(localizations.rename, style: const TextStyle(fontSize: 13)),\n                onTap: () => renameHistory(storage, item)),\n            CustomPopupMenuItem(\n                height: 35,\n                child: Text(localizations.export, style: const TextStyle(fontSize: 13)),\n                onTap: () => export(item)),\n            const PopupMenuDivider(height: 3),\n            CustomPopupMenuItem(\n                height: 35,\n                child: Text(localizations.repeatAllRequests, style: const TextStyle(fontSize: 13)),\n                onTap: () async {\n                  var requests = (await storage.getRequests(item)).reversed;\n                  //重发所有请求\n                  _repeatAllRequests(requests.toList());\n                }),\n            const PopupMenuDivider(height: 3),\n            CustomPopupMenuItem(\n                height: 35,\n                child: Text(localizations.delete, style: const TextStyle(fontSize: 13)),\n                onTap: () {\n                  if (item == widget.historyTask.history) {\n                    widget.historyTask.cancelTask();\n                  }\n                  storage.removeHistory(index);\n                  FlutterToastr.show(localizations.deleteSuccess, context);\n                }),\n          ]).whenComplete(() => setState(() => selectIndex = -1));\n        },\n        child: ListTile(\n            selected: selectIndex == index,\n            dense: true,\n            title: Text(item.name),\n            subtitle: Text(localizations.historySubtitle(item.requestLength, item.size)),\n            onTap: () => toRequestsView(item)));\n  }\n\n  void toRequestsView(HistoryItem item) {\n    Navigator.pushNamed(context, \"/request_list\", arguments: {'item': item}).whenComplete(() async {\n      if (item != widget.historyTask.history && item.requests != null && item.requestLength != item.requests?.length) {\n        await widget.storage.flushRequests(item, item.requests!);\n        setState(() {});\n      }\n      Future.delayed(const Duration(seconds: 60), () => item.requests = null);\n    });\n  }\n\n  //重命名\n  void renameHistory(HistoryStorage storage, HistoryItem item) {\n    String name = item.name;\n    showDialog(\n        context: context,\n        builder: (context) {\n          return AlertDialog(\n            content: TextFormField(\n              initialValue: name,\n              decoration: InputDecoration(label: Text(localizations.name)),\n              onChanged: (val) => name = val,\n            ),\n            actions: <Widget>[\n              TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n              TextButton(\n                child: Text(localizations.save),\n                onPressed: () {\n                  if (name.isEmpty) {\n                    FlutterToastr.show(localizations.historyEmptyName, context, position: 2);\n                    return;\n                  }\n                  Navigator.maybePop(context);\n                  setState(() {\n                    item.name = name;\n                    storage.refresh();\n                  });\n                },\n              ),\n            ],\n          );\n        });\n  }\n\n  //导出har\n  Future<void> export(HistoryItem item) async {\n    //文件名称\n    String fileName =\n        '${item.name.contains(\"ProxyPin\") ? '' : 'ProxyPin'}${item.name}.har'.replaceAll(\" \", \"_\").replaceAll(\":\", \"_\");\n\n    final String? path = await FilePicker.platform.saveFile(fileName: fileName);\n    if (path == null) {\n      return;\n    }\n\n    //获取请求\n    List<HttpRequest> requests = await storage.getRequests(item);\n    var file = await File(path).create();\n    await Har.writeFile(requests, file, title: item.name);\n    if (mounted) FlutterToastr.show(localizations.exportSuccess, context);\n    Future.delayed(const Duration(seconds: 30), () => item.requests = null);\n  }\n\n  ///重发所有请求\n  void _repeatAllRequests(Iterable<HttpRequest> requests) async {\n    var localizations = AppLocalizations.of(context);\n\n    for (var request in requests) {\n      var httpRequest = request.copy(uri: request.requestUrl);\n      var proxyInfo = proxyServer.isRunning ? ProxyInfo.of(\"127.0.0.1\", proxyServer.port) : null;\n      try {\n        await HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo, timeout: const Duration(seconds: 3));\n        if (mounted) {\n          FlutterToastr.show(localizations!.reSendRequest, rootNavigator: true, context);\n        }\n      } catch (e) {\n        if (mounted) {\n          FlutterToastr.show('${localizations!.fail} $e', rootNavigator: true, context);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/left_menus/navigation.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/desktop/preference.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\n///左侧导航栏\n/// @author wanghongen\n/// 2024/8/6\nclass LeftNavigationBar extends StatefulWidget {\n  final AppConfiguration appConfiguration;\n  final ProxyServer proxyServer;\n  final ValueNotifier<int> selectIndex;\n\n  const LeftNavigationBar(\n      {super.key, required this.appConfiguration, required this.proxyServer, required this.selectIndex});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _LeftNavigationBarState();\n  }\n}\n\nclass _LeftNavigationBarState extends State<LeftNavigationBar> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  List<NavigationRailDestination> get destinations => [\n        NavigationRailDestination(\n            padding: const EdgeInsets.only(bottom: 5),\n            icon: Icon(Icons.workspaces_outlined),\n            label: Text(localizations.requests, style: Theme.of(context).textTheme.bodySmall)),\n        NavigationRailDestination(\n            padding: const EdgeInsets.only(bottom: 5),\n            icon: Icon(Icons.favorite_outline_outlined),\n            label: Text(localizations.favorites, style: Theme.of(context).textTheme.bodySmall)),\n        NavigationRailDestination(\n            padding: const EdgeInsets.only(bottom: 5),\n            icon: Icon(Icons.history_outlined),\n            label: Text(localizations.history, style: Theme.of(context).textTheme.bodySmall)),\n        NavigationRailDestination(\n            padding: const EdgeInsets.only(bottom: 5),\n            icon: Icon(Icons.hardware_outlined),\n            label: Text(localizations.toolbox, style: Theme.of(context).textTheme.bodySmall)),\n      ];\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder(\n        valueListenable: widget.selectIndex,\n        builder: (_, index, __) {\n          if (index == -1) {\n            return const SizedBox();\n          }\n\n          return Container(\n            width: localizations.localeName == 'en' ? 70 : 57,\n            decoration:\n                BoxDecoration(border: Border(right: BorderSide(color: Theme.of(context).dividerColor, width: 0.2))),\n            child: Column(children: <Widget>[\n              SizedBox(\n                height: 320,\n                child: leftNavigation(index),\n              ),\n              Expanded(\n                  child: Column(\n                mainAxisAlignment: MainAxisAlignment.end,\n                children: [\n                  Tooltip(\n                      message: localizations.preference,\n                      preferBelow: false,\n                      child: IconButton(\n                          iconSize: 22,\n                          onPressed: () {\n                            showDialog(\n                                context: context,\n                                builder: (_) => Preference(widget.appConfiguration, widget.proxyServer.configuration));\n                          },\n                          icon: Icon(Icons.settings_outlined, color: Colors.grey.shade500))),\n                  const SizedBox(height: 5),\n                  Tooltip(\n                      preferBelow: true,\n                      message: localizations.feedback,\n                      child: IconButton(\n                        iconSize: 22,\n                        onPressed: () => launchUrl(Uri.parse(\"https://github.com/wanghongenpin/proxypin/issues\")),\n                        icon: Icon(Icons.feedback_outlined, color: Colors.grey.shade500),\n                      )),\n                  const SizedBox(height: 10),\n                ],\n              ))\n            ]),\n          );\n        });\n  }\n\n  //left menu eg: requests, favorites, history, toolbox\n  Widget leftNavigation(int index) {\n    return NavigationRail(\n        minWidth: 57,\n        backgroundColor: Theme.of(context).scaffoldBackgroundColor,\n        selectedIconTheme: IconTheme.of(context).copyWith(color: Theme.of(context).colorScheme.primary, size: 22),\n        unselectedIconTheme:\n            IconTheme.of(context).copyWith(color: IconTheme.of(context).color?.withValues(alpha: 0.55), size: 22),\n        labelType: NavigationRailLabelType.all,\n        destinations: destinations,\n        selectedIndex: index,\n        onDestinationSelected: (int index) {\n          widget.selectIndex.value = index;\n        });\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/preference.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/configuration.dart';\n\n/// @author wanghongen\n/// 2024/1/2\nclass Preference extends StatefulWidget {\n  final Configuration configuration;\n  final AppConfiguration appConfiguration;\n\n  const Preference(this.appConfiguration, this.configuration, {super.key});\n\n  @override\n  State<StatefulWidget> createState() => _PreferenceState();\n}\n\nclass _PreferenceState extends State<Preference> {\n  late Configuration configuration;\n  late AppConfiguration appConfiguration;\n\n  final memoryCleanupController = TextEditingController();\n  final memoryCleanupList = [null, 512, 1024, 2048, 4096];\n\n  @override\n  void initState() {\n    super.initState();\n    configuration = widget.configuration;\n    appConfiguration = widget.appConfiguration;\n    if (!memoryCleanupList.contains(appConfiguration.memoryCleanupThreshold)) {\n      memoryCleanupController.text = appConfiguration.memoryCleanupThreshold.toString();\n    }\n  }\n\n  @override\n  void dispose() {\n    memoryCleanupController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    var titleStyle = Theme.of(context).textTheme.titleSmall;\n    var subtitleStyle = TextStyle(fontSize: 12, color: Colors.grey);\n\n    return AlertDialog(\n        scrollable: true,\n        title: Row(children: [\n          const Icon(Icons.settings, size: 20),\n          const SizedBox(width: 10),\n          Text(localizations.preference, style: Theme.of(context).textTheme.titleMedium),\n          const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))\n        ]),\n        content: SizedBox(\n            width: 400,\n            child: Column(children: [\n              Row(children: [\n                SizedBox(width: 100, child: Text(\"${localizations.language}: \", style: titleStyle)),\n                DropdownButton<Locale>(\n                    value: appConfiguration.language,\n                    onChanged: (Locale? value) => appConfiguration.language = value,\n                    focusColor: Colors.transparent,\n                    items: [\n                      DropdownMenuItem(value: null, child: Text(localizations.followSystem)),\n                      const DropdownMenuItem(value: Locale.fromSubtags(languageCode: \"zh\"), child: Text(\"简体中文\")),\n                      const DropdownMenuItem(\n                          value: Locale.fromSubtags(languageCode: \"zh\", scriptCode: \"Hant\"), child: Text(\"繁體中文\")),\n                      const DropdownMenuItem(value: Locale.fromSubtags(languageCode: \"en\"), child: Text(\"English\")),\n                    ]),\n              ]),\n              //主题\n              Row(children: [\n                SizedBox(width: 100, child: Text(\"${localizations.theme}: \", style: titleStyle)),\n                DropdownButton<ThemeMode>(\n                    value: appConfiguration.themeMode,\n                    onChanged: (ThemeMode? value) => appConfiguration.themeMode = value!,\n                    focusColor: Colors.transparent,\n                    items: [\n                      DropdownMenuItem(value: ThemeMode.system, child: Text(localizations.followSystem)),\n                      DropdownMenuItem(value: ThemeMode.light, child: Text(localizations.themeLight)),\n                      DropdownMenuItem(value: ThemeMode.dark, child: Text(localizations.themeDark)),\n                    ]),\n              ]),\n              Tooltip(\n                  message: localizations.material3,\n                  child: Row(\n                    children: [\n                      SizedBox(width: 100, child: Text(\"Material3: \", style: titleStyle)),\n                      Transform.scale(\n                          scale: 0.75,\n                          child: Switch(\n                            value: appConfiguration.useMaterial3,\n                            onChanged: (bool value) => appConfiguration.useMaterial3 = value,\n                          ))\n                    ],\n                  )),\n              //主题颜色\n              Row(children: [\n                SizedBox(\n                    width: 120,\n                    child: Text(\"${localizations.themeColor}: \", style: titleStyle, textAlign: TextAlign.start)),\n              ]),\n              themeColor(context),\n              const Divider(),\n              ListTile(\n                  contentPadding: EdgeInsets.zero,\n                  title: Text(localizations.autoStartup, style: titleStyle),\n                  //默认是否启动\n                  subtitle: Text(localizations.autoStartupDescribe, style: subtitleStyle),\n                  trailing: SwitchWidget(\n                      scale: 0.75,\n                      value: configuration.startup,\n                      onChanged: (value) {\n                        configuration.startup = value;\n                        configuration.flushConfig();\n                      })),\n              ListTile(\n                  contentPadding: EdgeInsets.zero,\n                  title: Text(localizations.headerExpanded, style: titleStyle),\n                  subtitle: Text(localizations.headerExpandedSubtitle, style: subtitleStyle),\n                  trailing: SwitchWidget(\n                      scale: 0.75,\n                      value: appConfiguration.headerExpanded,\n                      onChanged: (value) {\n                        appConfiguration.headerExpanded = value;\n                        appConfiguration.flushConfig();\n                      })),\n              ListTile(\n                  contentPadding: EdgeInsets.zero,\n                  title: Text(localizations.memoryCleanup, style: titleStyle),\n                  subtitle: Text(localizations.memoryCleanupSubtitle, style: subtitleStyle),\n                  trailing: memoryCleanup(context, localizations)),\n\n              SizedBox(height: 5),\n            ])));\n  }\n\n  ///主题颜色\n  Widget themeColor(BuildContext context) {\n    return Wrap(\n      children: ColorMapping.colors.entries.map((pair) {\n        var dividerColor = Theme.of(context).focusColor;\n        var background = appConfiguration.themeColor == pair.value ? dividerColor : Colors.transparent;\n\n        return GestureDetector(\n            onTap: () => appConfiguration.setThemeColor = pair.key,\n            child: Tooltip(\n              message: pair.key,\n              child: Container(\n                margin: const EdgeInsets.all(4.0),\n                decoration: BoxDecoration(\n                  color: background,\n                  border: Border.all(color: Colors.transparent, width: 8),\n                ),\n                child: Dot(color: pair.value, size: 15),\n              ),\n            ));\n      }).toList(),\n    );\n  }\n\n  bool memoryCleanupOpened = false;\n\n  ///内存清理\n  Widget memoryCleanup(BuildContext context, AppLocalizations localizations) {\n    try {\n      return DropdownButton<int>(\n          value: appConfiguration.memoryCleanupThreshold,\n          onTap: () {\n            memoryCleanupOpened = true;\n          },\n          onChanged: (val) {\n            memoryCleanupOpened = false;\n            setState(() {\n              appConfiguration.memoryCleanupThreshold = val;\n            });\n            appConfiguration.flushConfig();\n          },\n          underline: Container(),\n          items: [\n            DropdownMenuItem(value: null, child: Text(localizations.unlimited)),\n            const DropdownMenuItem(value: 512, child: Text(\"512M\")),\n            const DropdownMenuItem(value: 1024, child: Text(\"1024M\")),\n            const DropdownMenuItem(value: 2048, child: Text(\"2048M\")),\n            const DropdownMenuItem(value: 4096, child: Text(\"4096M\")),\n            DropdownMenuInputItem(\n                controller: memoryCleanupController,\n                child: Container(\n                    constraints: BoxConstraints(maxWidth: 65, minWidth: 35),\n                    child: TextField(\n                        controller: memoryCleanupController,\n                        onSubmitted: (value) {\n                          setState(() {});\n                          appConfiguration.memoryCleanupThreshold = int.tryParse(value);\n                          appConfiguration.flushConfig();\n\n                          if (memoryCleanupOpened) {\n                            memoryCleanupOpened = false;\n                            Navigator.pop(context);\n                            return;\n                          }\n                        },\n                        inputFormatters: [\n                          LengthLimitingTextInputFormatter(5),\n                          FilteringTextInputFormatter.allow(RegExp(\"[0-9]\"))\n                        ],\n                        decoration: InputDecoration(hintText: localizations.custom, suffixText: \"M\")))),\n          ]);\n    } catch (e) {\n      appConfiguration.memoryCleanupThreshold = null;\n      logger.e('memory button build error', error: e, stackTrace: StackTrace.current);\n      return const SizedBox();\n    }\n  }\n}\n\nclass DropdownMenuInputItem extends DropdownMenuItem<int> {\n  final TextEditingController controller;\n\n  @override\n  int? get value => int.tryParse(controller.text) ?? 0;\n\n  const DropdownMenuInputItem({super.key, required this.controller, required super.child});\n}\n"
  },
  {
    "path": "lib/ui/desktop/request/domians.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:collection';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_desktop_context_menu/flutter_desktop_context_menu.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/components/host_filter.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/ui/component/transition.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/content/panel.dart';\nimport 'package:proxypin/ui/desktop/request/request.dart';\nimport 'package:proxypin/utils/keyword_highlight.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\n\nimport '../../component/model/search_model.dart';\n\n/// 左侧域名\n/// @author wanghongen\n/// 2023/10/8\nclass DomainList extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final NetworkTabController panel;\n\n  final ListenableList<HttpRequest> list;\n  final bool shrinkWrap;\n  final Function(List<HttpRequest>)? onRemove;\n\n  const DomainList(\n      {super.key,\n      required this.proxyServer,\n      required this.list,\n      this.shrinkWrap = true,\n      required this.panel,\n      this.onRemove});\n\n  @override\n  State<StatefulWidget> createState() {\n    return DomainWidgetState();\n  }\n}\n\nclass DomainWidgetState extends State<DomainList> with AutomaticKeepAliveClientMixin {\n  //域名和对应请求列表的映射\n  final LinkedHashMap<String, DomainRequests> containerMap = LinkedHashMap<String, DomainRequests>();\n\n  //搜索视图\n  LinkedHashMap<String, DomainRequests> searchView = LinkedHashMap<String, DomainRequests>();\n\n  //搜索的内容\n  SearchModel? searchModel;\n  bool changing = false; //是否存在刷新任务\n  //关键词高亮监听\n  late VoidCallback highlightListener;\n\n  bool sortDesc = true;\n\n  void changeState() {\n    if (!changing) {\n      changing = true;\n      Future.delayed(const Duration(milliseconds: 500), () {\n        setState(() {\n          changing = false;\n        });\n      });\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    var container = widget.list;\n    for (var request in container.source) {\n      DomainRequests domainRequests = getDomainRequests(request);\n      domainRequests.addRequest(request.requestId, request, sortDesc);\n    }\n    highlightListener = () {\n      //回调时机在高亮设置页面dispose之后。所以需要在下一帧刷新，否则会报错\n      WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n        highlightHandler();\n      });\n    };\n    KeywordHighlights.addListener(highlightListener);\n  }\n\n  @override\n  dispose() {\n    KeywordHighlights.removeListener(highlightListener);\n    super.dispose();\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    var list = containerMap.values;\n\n    //根究搜素文本过滤\n    if (searchModel?.isNotEmpty == true) {\n      searchView = searchFilter(searchModel!);\n      list = searchView.values;\n    } else {\n      searchView.clear();\n    }\n\n    return widget.shrinkWrap\n        ? SingleChildScrollView(child: Column(children: list.toList()))\n        : ListView.builder(itemCount: list.length, itemBuilder: (_, index) => list.elementAt(index));\n  }\n\n  ///搜索\n  void search(SearchModel? val) {\n    setState(() {\n      searchModel = val;\n    });\n  }\n\n  ///搜索过滤\n  LinkedHashMap<String, DomainRequests> searchFilter(SearchModel searchModel) {\n    LinkedHashMap<String, DomainRequests> result = LinkedHashMap<String, DomainRequests>();\n\n    containerMap.forEach((key, domainRequests) {\n      var body = domainRequests.search(searchModel);\n      if (body.isNotEmpty) {\n        result[key] = domainRequests.copy(body: body, selected: searchView[key]?.currentSelected);\n      }\n    });\n\n    return result;\n  }\n\n  ///高亮处理\n  highlightHandler() {\n    //获取所有请求Widget\n    List<RequestWidget> requests = containerMap.values.map((e) => e.body).expand((element) => element).toList();\n    for (RequestWidget request in requests) {\n      GlobalKey key = request.key as GlobalKey<State>;\n      key.currentState?.setState(() {});\n    }\n  }\n\n  ///添加请求\n  add(Channel channel, HttpRequest request) {\n    String? host = request.remoteDomain();\n    if (host == null) {\n      return;\n    }\n\n    //按照域名分类\n    DomainRequests domainRequests = getDomainRequests(request);\n    var isNew = domainRequests.body.isEmpty;\n\n    domainRequests.addRequest(request.requestId, request, sortDesc);\n    //搜索视图\n    if (searchModel?.isNotEmpty == true && searchModel?.filter(request, null) == true) {\n      searchView[host]?.addRequest(request.requestId, request, sortDesc);\n    }\n\n    if (isNew) {\n      setState(() {\n        containerMap[host] = domainRequests;\n      });\n    }\n  }\n\n  DomainRequests getDomainRequests(HttpRequest request) {\n    var host = request.remoteDomain()!;\n    DomainRequests? domainRequests = containerMap[host];\n    if (domainRequests == null) {\n      domainRequests = DomainRequests(\n        host,\n        proxyServer: widget.proxyServer,\n        trailing: appIcon(request),\n        onDelete: deleteHost,\n        onRequestRemove: (req) {\n          widget.onRemove?.call([req]);\n          changeState();\n        },\n      );\n      containerMap[host] = domainRequests;\n    }\n\n    return domainRequests;\n  }\n\n  Widget? appIcon(HttpRequest request) {\n    var processInfo = request.processInfo;\n    if (processInfo == null) {\n      return null;\n    }\n\n    return futureWidget(\n        processInfo.getIcon(),\n        (data) =>\n            data.isEmpty ? const SizedBox() : Image.memory(data, width: 23, height: Platform.isWindows ? 16 : null));\n  }\n\n  ///移除域名\n  deleteHost(String host) {\n    DomainRequests? domainRequests = containerMap.remove(host);\n    if (domainRequests == null) {\n      return;\n    }\n    setState(() {});\n\n    widget.onRemove?.call(domainRequests.body.map((e) => e.request).toList());\n  }\n\n  ///添加响应\n  void addResponse(ChannelContext channelContext, HttpResponse response) {\n    String domain = channelContext.host!.domain;\n    DomainRequests? domainRequests = containerMap[domain];\n    var pathRow = domainRequests?.getRequest(response);\n    pathRow?.setResponse(response);\n    if (pathRow == null) {\n      return;\n    }\n\n    //搜索视图\n    if (searchModel?.isNotEmpty == true && searchModel?.filter(pathRow.request, response) == true) {\n      var requests = searchView[domain];\n      if (requests?.getRequest(response) == null) {\n        requests?.addRequest(response.requestId, pathRow.request, sortDesc);\n      }\n      requests?.getRequest(response)?.setResponse(response);\n    }\n  }\n\n  remove(List<HttpRequest> list) {\n    for (var request in list) {\n      String? host = request.remoteDomain();\n      containerMap[host]?._removeRequest(request);\n    }\n  }\n\n  ///清理\n  clean() {\n    setState(() {\n      containerMap.clear();\n      searchView.clear();\n\n      var container = widget.list;\n      for (var request in container.source) {\n        DomainRequests domainRequests = getDomainRequests(request);\n        domainRequests.addRequest(request.requestId, request, sortDesc);\n      }\n    });\n  }\n\n  List<HttpRequest> currentView() {\n    var container = containerMap.values;\n    if (searchModel?.isNotEmpty == true) {\n      container = searchView.values;\n    }\n    return container.expand((list) => list.body.map((it) => it.request)).toList();\n  }\n\n  ///排序\n  sort(bool desc) {\n    sortDesc = desc;\n    containerMap.forEach((key, request) {\n      var reversed = request.body.toList().reversed;\n      request.body.clear();\n      request.body.addAll(reversed);\n      request.changeState();\n    });\n  }\n}\n\n///标题和内容布局 标题是域名 内容是域名下请求\nclass DomainRequests extends StatefulWidget {\n  //请求ID和请求的映射\n  final Map<String, RequestWidget> requestMap = HashMap<String, RequestWidget>();\n\n  final String domain;\n  final ProxyServer proxyServer;\n  final Widget? trailing;\n\n  //请求列表\n  final Queue<RequestWidget> body = Queue();\n\n  //是否选中\n  final bool selected;\n\n  //移除回调\n  final Function(String host)? onDelete;\n  final Function(HttpRequest request)? onRequestRemove;\n\n  DomainRequests(this.domain,\n      {this.selected = false, this.onDelete, required this.proxyServer, this.onRequestRemove, this.trailing})\n      : super(key: GlobalKey<_DomainRequestsState>());\n\n  ///添加请求\n  void addRequest(String? requestId, HttpRequest request, bool sortDesc) {\n    if (requestMap.containsKey(requestId)) return;\n\n    var requestWidget = RequestWidget(request,\n        index: body.length, proxyServer: proxyServer, displayDomain: false, remove: (it) => _remove(it));\n    sortDesc ? body.addFirst(requestWidget) : body.addLast(requestWidget);\n\n    if (requestId == null) {\n      return;\n    }\n\n    requestMap[requestId] = requestWidget;\n    changeState();\n  }\n\n  RequestWidget? getRequest(HttpResponse response) {\n    return requestMap[response.request?.requestId ?? response.requestId];\n  }\n\n  setTrailing(Widget? trailing) {\n    var state = key as GlobalKey<_DomainRequestsState>;\n    state.currentState?.trailing = trailing;\n  }\n\n  _remove(RequestWidget requestWidget) {\n    if (body.remove(requestWidget)) {\n      onRequestRemove?.call(requestWidget.request);\n      changeState();\n    }\n  }\n\n  _removeRequest(HttpRequest request) {\n    var requestWidget = requestMap.remove(request.requestId);\n    if (requestWidget != null) {\n      _remove(requestWidget);\n    }\n  }\n\n  ///根据文本过滤\n  Iterable<RequestWidget> search(SearchModel searchModel) {\n    return body\n        .where((element) => searchModel.filter(element.request, element.response.get() ?? element.request.response));\n  }\n\n  ///复制\n  DomainRequests copy({Iterable<RequestWidget>? body, bool? selected}) {\n    var state = key as GlobalKey<_DomainRequestsState>;\n    var headerBody = DomainRequests(domain,\n        trailing: trailing,\n        selected: selected ?? state.currentState?.selected == true,\n        onDelete: onDelete,\n        onRequestRemove: onRequestRemove,\n        proxyServer: proxyServer);\n    if (body != null) {\n      headerBody.body.addAll(body);\n    }\n    return headerBody;\n  }\n\n  bool get currentSelected {\n    var state = key as GlobalKey<_DomainRequestsState>;\n    return state.currentState?.selected == true;\n  }\n\n  changeState() {\n    var state = key as GlobalKey<_DomainRequestsState>;\n    state.currentState?.changeState();\n  }\n\n  @override\n  State<StatefulWidget> createState() {\n    return _DomainRequestsState();\n  }\n}\n\nclass _DomainRequestsState extends State<DomainRequests> {\n  final GlobalKey<ColorTransitionState> transitionState = GlobalKey<ColorTransitionState>();\n  late Configuration configuration;\n  late bool selected;\n  Widget? trailing;\n  bool changing = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    configuration = widget.proxyServer.configuration;\n    selected = widget.selected;\n    trailing = widget.trailing;\n  }\n\n  changeState() {\n    //防止频繁刷新\n    if (!changing) {\n      changing = true;\n      Future.delayed(const Duration(milliseconds: 500), () {\n        if (mounted) {\n          setState(() {\n            changing = false;\n          });\n          transitionState.currentState?.show();\n        }\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(children: [\n      _hostWidget(widget.domain),\n      Offstage(offstage: !selected, child: Column(children: widget.body.toList()))\n    ]);\n  }\n\n  //domain title\n  Widget _hostWidget(String title) {\n    var host = GestureDetector(\n        onSecondaryTap: menu,\n        child: ListTile(\n            minLeadingWidth: 25,\n            leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 18),\n            trailing: trailing,\n            dense: true,\n            horizontalTitleGap: 0,\n            contentPadding: const EdgeInsets.only(left: 3, right: 8),\n            visualDensity: const VisualDensity(vertical: -3.6),\n            title: Text(title,\n                textAlign: TextAlign.left,\n                style: const TextStyle(fontSize: 14),\n                maxLines: 1,\n                overflow: TextOverflow.ellipsis),\n            onTap: () {\n              setState(() {\n                selected = !selected;\n              });\n            }));\n\n    return ColorTransition(\n        key: transitionState,\n        duration: const Duration(milliseconds: 1800),\n        begin: Theme.of(context).focusColor,\n        startAnimation: false,\n        child: host);\n  }\n\n  //域名右键菜单\n  menu() {\n    Menu menu = Menu(items: [\n      MenuItem(\n          label: localizations.copyHost,\n          onClick: (_) {\n            Clipboard.setData(ClipboardData(text: Uri.parse(widget.domain).host));\n            FlutterToastr.show(localizations.copied, context);\n          }),\n      MenuItem.separator(),\n      MenuItem(\n        label: localizations.domainFilter,\n        type: 'submenu',\n        submenu: hostFilterMenu(),\n      ),\n      MenuItem.separator(),\n      MenuItem(label: localizations.repeatDomainRequests, onClick: (_) => repeatDomainRequests()),\n      MenuItem.separator(),\n      MenuItem(label: localizations.delete, onClick: (_) => _delete()),\n    ]);\n\n    popUpContextMenu(menu);\n  }\n\n  //重复域名下请求\n  void repeatDomainRequests() async {\n    var list = widget.body.toList().reversed;\n    for (var requestWidget in list) {\n      var request = requestWidget.request.copy(uri: requestWidget.request.requestUrl);\n      var proxyInfo = widget.proxyServer.isRunning ? ProxyInfo.of(\"127.0.0.1\", widget.proxyServer.port) : null;\n      try {\n        await HttpClients.proxyRequest(request, proxyInfo: proxyInfo, timeout: const Duration(seconds: 3));\n        if (mounted) FlutterToastr.show(localizations.reSendRequest, rootNavigator: true, context);\n      } catch (e) {\n        if (mounted) FlutterToastr.show('${localizations.fail}$e', rootNavigator: true, context);\n      }\n    }\n  }\n\n  Menu hostFilterMenu() {\n    return Menu(items: [\n      MenuItem(\n          label: localizations.domainBlacklist,\n          onClick: (_) {\n            HostFilter.blacklist.add(Uri.parse(widget.domain).host);\n            configuration.flushConfig();\n            FlutterToastr.show(localizations.addSuccess, context);\n          }),\n      MenuItem(\n          label: localizations.domainWhitelist,\n          onClick: (_) {\n            HostFilter.whitelist.add(Uri.parse(widget.domain).host);\n            configuration.flushConfig();\n            FlutterToastr.show(localizations.addSuccess, context);\n          }),\n      MenuItem(\n          label: localizations.deleteWhitelist,\n          onClick: (_) {\n            HostFilter.whitelist.remove(Uri.parse(widget.domain).host);\n            configuration.flushConfig();\n            FlutterToastr.show(localizations.deleteSuccess, context);\n          }),\n    ]);\n  }\n\n  _delete() {\n    widget.onDelete?.call(widget.domain);\n    widget.requestMap.clear();\n    widget.body.clear();\n    FlutterToastr.show(localizations.deleteSuccess, context);\n  }\n}\n\nclass HostWidget extends StatelessWidget {\n  final String host;\n  final Function()? onMenu;\n\n  const HostWidget(this.host, {super.key, this.onMenu});\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n        onSecondaryTap: onMenu,\n        child: ListTile(\n            minLeadingWidth: 25,\n            leading: const Icon(Icons.arrow_right, size: 18),\n            dense: true,\n            horizontalTitleGap: 0,\n            contentPadding: const EdgeInsets.only(left: 3, right: 8),\n            visualDensity: const VisualDensity(vertical: -3.6),\n            title: Text(host,\n                textAlign: TextAlign.left,\n                style: const TextStyle(fontSize: 14),\n                maxLines: 1,\n                overflow: TextOverflow.ellipsis)));\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/request/list.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:io';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/content/panel.dart';\nimport 'package:proxypin/ui/desktop/request/request_sequence.dart';\nimport 'package:proxypin/ui/desktop/request/search.dart';\nimport 'package:proxypin/utils/har.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\n\nimport '../../component/model/search_model.dart';\nimport 'domians.dart';\nimport 'package:proxypin/ui/desktop/request/report_servers.dart';\n\n/// @author wanghongen\nclass DesktopRequestListWidget extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final ListenableList<HttpRequest>? list;\n  final NetworkTabController panel;\n\n  const DesktopRequestListWidget({super.key, required this.proxyServer, this.list, required this.panel});\n\n  @override\n  State<StatefulWidget> createState() {\n    return DesktopRequestListState();\n  }\n}\n\nclass DesktopRequestListState extends State<DesktopRequestListWidget> with AutomaticKeepAliveClientMixin {\n  final GlobalKey<RequestSequenceState> requestSequenceKey = GlobalKey<RequestSequenceState>();\n  final GlobalKey<DomainWidgetState> domainListKey = GlobalKey<DomainWidgetState>();\n  final GlobalKey<SearchState> searchKey = GlobalKey<SearchState>();\n\n  //请求列表容器\n  ListenableList<HttpRequest> container = ListenableList();\n\n  bool sortDesc = true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.list != null) {\n      container = widget.list!;\n    }\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void dispose() {\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    List<Tab> tabs = [\n      Tab(child: Text(localizations.domainList, style: const TextStyle(fontSize: 13))),\n      Tab(child: Text(localizations.sequence, style: const TextStyle(fontSize: 13))),\n    ];\n\n    return DefaultTabController(\n        length: tabs.length,\n        child: Scaffold(\n            appBar: AppBar(\n              toolbarHeight: 40,\n              title: SizedBox(height: 40, child: TabBar(tabs: tabs, dividerColor: Colors.transparent)),\n              automaticallyImplyLeading: false,\n              actions: [popupMenus()],\n            ),\n            bottomNavigationBar: Search(key: searchKey, onSearch: search),\n            body: Padding(\n                padding: const EdgeInsets.only(right: 5),\n                child: TabBarView(physics: const NeverScrollableScrollPhysics(), children: [\n                  DomainList(\n                      key: domainListKey,\n                      list: container,\n                      panel: widget.panel,\n                      proxyServer: widget.proxyServer,\n                      onRemove: domainListRemove),\n                  RequestSequence(\n                      key: requestSequenceKey,\n                      container: container,\n                      proxyServer: widget.proxyServer,\n                      onRemove: sequenceRemove),\n                ]))));\n  }\n\n  Widget popupMenus() {\n    return PopupMenuButton(\n        offset: const Offset(0, 32),\n        icon: const Icon(Icons.more_vert_outlined, size: 20),\n        itemBuilder: (BuildContext context) {\n          return <PopupMenuEntry>[\n            CustomPopupMenuItem(\n                height: 37,\n                onTap: () => searchKey.currentState?.searchDialog(),\n                child: IconText(\n                    icon: const Icon(Icons.search, size: 17),\n                    text: localizations.search,\n                    textStyle: const TextStyle(fontSize: 13))),\n            CustomPopupMenuItem(\n                height: 37,\n                onTap: () => export('ProxyPin_${DateTime.now().dateFormat()}.har'),\n                child: IconText(\n                    icon: const Icon(Icons.share, size: 16),\n                    text: localizations.viewExport,\n                    textStyle: const TextStyle(fontSize: 13))),\n            CustomPopupMenuItem(\n                height: 37,\n                onTap: () => repeatAllRequests(),\n                child: IconText(\n                    icon: const Icon(Icons.repeat, size: 16),\n                    text: localizations.repeatAllRequests,\n                    textStyle: const TextStyle(fontSize: 13))),\n            CustomPopupMenuItem(\n                height: 37,\n                onTap: () {\n                  sortDesc = !sortDesc;\n                  requestSequenceKey.currentState?.sort(sortDesc);\n                  domainListKey.currentState?.sort(sortDesc);\n                },\n                child: IconText(\n                    icon: const Icon(Icons.sort, size: 16),\n                    text: sortDesc ? localizations.timeAsc : localizations.timeDesc,\n                    textStyle: const TextStyle(fontSize: 13))),\n            CustomPopupMenuItem(\n                height: 37,\n                onTap: () {\n                  showReportServersDialog(context);\n                },\n                child: IconText(\n                    icon: Icon(Icons.cloud_upload_outlined, size: 16),\n                    text: localizations.reportServers,\n                    textStyle: TextStyle(fontSize: 13))),\n          ];\n        });\n  }\n\n  ///添加请求\n  void add(Channel channel, HttpRequest request) {\n    container.add(request);\n    domainListKey.currentState?.add(channel, request);\n    requestSequenceKey.currentState?.add(request);\n  }\n\n  ///添加响应\n  void addResponse(ChannelContext channelContext, HttpResponse response) {\n    domainListKey.currentState?.addResponse(channelContext, response);\n    requestSequenceKey.currentState?.addResponse(response);\n  }\n\n  ///移除\n  void domainListRemove(List<HttpRequest> list) {\n    container.removeWhere((element) => list.contains(element));\n    requestSequenceKey.currentState?.remove(list);\n  }\n\n  ///全部请求删除\n  void sequenceRemove(List<HttpRequest> list) {\n    container.removeWhere((element) => list.contains(element));\n    domainListKey.currentState?.remove(list);\n  }\n\n  void search(SearchModel searchModel) {\n    domainListKey.currentState?.search(searchModel);\n    requestSequenceKey.currentState?.search(searchModel);\n  }\n\n  List<HttpRequest>? currentView() {\n    return domainListKey.currentState?.currentView();\n  }\n\n  ///清理\n  void clean() {\n    setState(() {\n      container.clear();\n      domainListKey.currentState?.clean();\n      requestSequenceKey.currentState?.clean();\n      widget.panel.change(null, null);\n    });\n  }\n\n  void cleanupEarlyData(int retain) {\n    var list = container.source;\n    if (list.length <= retain) {\n      return;\n    }\n\n    container.removeRange(0, list.length - retain);\n\n    domainListKey.currentState?.clean();\n    requestSequenceKey.currentState?.clean();\n  }\n\n  ///导出\n  Future<void> export(String fileName) async {\n    var path = await FilePicker.platform.saveFile(fileName: fileName);\n    if (path == null) {\n      return;\n    }\n\n    //获取请求\n    List<HttpRequest>? requests = currentView();\n    if (requests == null) return;\n\n    var file = await File(path).create();\n    await Har.writeFile(requests, file, title: fileName);\n\n    if (mounted) FlutterToastr.show(AppLocalizations.of(context)!.exportSuccess, context);\n  }\n\n  ///重发所有请求\n  void repeatAllRequests() async {\n    var requests = currentView();\n    if (requests == null) return;\n\n    var localizations = AppLocalizations.of(context);\n    final proxyServer = widget.proxyServer;\n\n    for (var request in requests) {\n      var httpRequest = request.copy(uri: request.requestUrl);\n      var proxyInfo = proxyServer.isRunning ? ProxyInfo.of(\"127.0.0.1\", proxyServer.port) : null;\n      try {\n        await HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo, timeout: const Duration(seconds: 3));\n        if (mounted) {\n          FlutterToastr.show(localizations!.reSendRequest, rootNavigator: true, context);\n        }\n      } catch (e) {\n        if (mounted) {\n          FlutterToastr.show('${localizations!.fail} $e', rootNavigator: true, context);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/request/repeat.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:math';\n\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\n///高级重放\n/// @author wanghongen\nclass CustomRepeatDialog extends StatefulWidget {\n  final Function onRepeat;\n  final SharedPreferences prefs;\n\n  const CustomRepeatDialog({super.key, required this.onRepeat, required this.prefs});\n\n  @override\n  State<StatefulWidget> createState() => _CustomRepeatState();\n}\n\nclass _CustomRepeatState extends State<CustomRepeatDialog> {\n  TextEditingController count = TextEditingController(text: '1');\n  TextEditingController interval = TextEditingController(text: '0');\n  TextEditingController minInterval = TextEditingController(text: '0');\n  TextEditingController maxInterval = TextEditingController(text: '1000');\n  TextEditingController delay = TextEditingController(text: '0');\n\n  bool fixed = true;\n  bool keepSetting = true;\n\n  DateTime? time;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  bool get isEN => Localizations.localeOf(context).languageCode == \"en\";\n\n  @override\n  void initState() {\n    super.initState();\n\n    var customerRepeat = widget.prefs.getString('customerRepeat');\n    keepSetting = customerRepeat != null;\n    if (customerRepeat != null) {\n      Map<String, dynamic> data = jsonDecode(customerRepeat);\n      count.text = data['count'];\n      interval.text = data['interval'];\n      minInterval.text = data['minInterval'];\n      maxInterval.text = data['maxInterval'];\n      delay.text = data['delay'];\n      fixed = data['fixed'] == true;\n    }\n  }\n\n  @override\n  void dispose() {\n    count.dispose();\n    interval.dispose();\n    delay.dispose();\n    super.dispose();\n  }\n\n\n  String _two(int v) => v.toString().padLeft(2, '0');\n\n  @override\n  Widget build(BuildContext context) {\n    final formKey = GlobalKey<FormState>();\n\n    return AlertDialog(\n      title: Text(localizations.customRepeat, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n      content: SingleChildScrollView(\n          child: Form(\n              key: formKey,\n              child: ListBody(\n                children: <Widget>[\n                  field(localizations.repeatCount, textField(count)), //次数\n                  const SizedBox(height: 8),\n                  Row(\n                    //间隔\n                    children: [\n                      SizedBox(width: isEN ? 100 : 90, child: Text(localizations.repeatInterval)),\n                      const SizedBox(height: 5),\n                      Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n                        //Checkbox样式 固定和随机\n                        Row(children: [\n                          SizedBox(\n                              width: isEN ? 107 : 84,\n                              height: 35,\n                              child: Transform.scale(\n                                  scale: 0.83,\n                                  child: CheckboxListTile(\n                                      contentPadding: EdgeInsets.zero,\n                                      title: Text(\"${localizations.fixed}:\"),\n                                      value: fixed,\n                                      dense: true,\n                                      onChanged: (val) {\n                                        setState(() {\n                                          fixed = true;\n                                        });\n                                      }))),\n                          SizedBox(width: 152, height: 32, child: textField(interval, style: const TextStyle(fontSize: 13))),\n                        ]),\n                        Row(children: [\n                          SizedBox(\n                              width: isEN ? 107 : 84,\n                              height: 35,\n                              child: Transform.scale(\n                                  scale: 0.83,\n                                  child: CheckboxListTile(\n                                      contentPadding: EdgeInsets.zero,\n                                      title: Text(\"${localizations.random}:\"),\n                                      value: !fixed,\n                                      dense: true,\n                                      onChanged: (val) {\n                                        setState(() {\n                                          fixed = false;\n                                        });\n                                      }))),\n                          SizedBox(width: 65, height: 32, child: textField(minInterval, style: const TextStyle(fontSize: 13))),\n                          const Padding(padding: EdgeInsets.symmetric(horizontal: 5), child: Text(\"-\")),\n                          SizedBox(width: 70, height: 32, child: textField(maxInterval, style: const TextStyle(fontSize: 13))),\n                        ]),\n                      ]),\n                    ],\n                  ),\n                  const SizedBox(height: 8),\n                  field(localizations.repeatDelay, textField(delay)), //延时\n                  const SizedBox(height: 8),\n                  field(\n                      localizations.scheduleTime,\n                      InkWell(\n                        onTap: _pickScheduleDateTime,\n                        child: Container(\n                          height: 42,\n                          padding: const EdgeInsets.only(left: 10, right: 10),\n                              decoration: BoxDecoration(\n                              border: Border.all(color: Theme.of(context).colorScheme.primary.withAlpha((0.5 * 255).round()), width: 1.0),\n                              borderRadius: BorderRadius.circular(4)),\n                          child: Row(\n                            children: [\n                              Text(time == null\n                                  ? ''\n                                  : \"${time!.year}-${_two(time!.month)}-${_two(time!.day)} ${_two(time!.hour)}:${_two(time!.minute)}\"),\n                              const Expanded(child: SizedBox()),\n                              if (time != null)\n                                InkWell(\n                                  onTap: () {\n                                    setState(() {\n                                      time = null;\n                                    });\n                                  },\n                                  child: const Icon(Icons.clear, size: 18),\n                                ),\n                              if (time == null) Icon(Icons.access_time, size: 18, color: Theme.of(context).colorScheme.primary),\n                            ],\n                          ),\n                        ),\n                      )), //指定时间\n                  const SizedBox(height: 8),\n                  //记录选择\n                  Row(mainAxisAlignment: MainAxisAlignment.start, children: [\n                    Text(localizations.keepCustomSettings),\n                    Expanded(\n                      child: Checkbox(\n                        value: keepSetting,\n                        onChanged: (val) {\n                          setState(() {\n                            keepSetting = val == true;\n                          });\n                        },\n                      ),\n                    ),\n                  ])\n                ],\n              ))),\n      actions: <Widget>[\n        TextButton(\n          child: Text(localizations.cancel),\n          onPressed: () => Navigator.of(context).pop(),\n        ),\n        TextButton(\n          child: Text(localizations.done),\n          onPressed: () {\n            if (!formKey.currentState!.validate()) {\n              return;\n            }\n            if (keepSetting) {\n              widget.prefs.setString(\n                  'customerRepeat',\n                  jsonEncode({\n                    'count': count.text,\n                    'interval': interval.text,\n                    'minInterval': minInterval.text,\n                    'maxInterval': maxInterval.text,\n                    'delay': delay.text,\n                    'fixed': fixed\n                  }));\n            } else {\n              widget.prefs.remove('customerRepeat');\n            }\n\n            int delayValue = int.parse(delay.text);\n            if (time != null) {\n              DateTime now = DateTime.now();\n              if (time!.isBefore(now)) {\n                time = time!.add(const Duration(days: 1));\n              }\n              delayValue += time!.difference(now).inMilliseconds;\n            }\n\n            //定时发起请求\n            Future.delayed(Duration(milliseconds: delayValue), () => submitTask(int.parse(count.text)));\n            Navigator.of(context).pop();\n          },\n        ),\n      ],\n    );\n  }\n\n  //定时重放\n  void submitTask(int counter) {\n    if (counter <= 0) {\n      return;\n    }\n    widget.onRepeat.call();\n\n    int intervalValue = int.parse(interval.text);\n    //随机\n    if (!fixed) {\n      int min = int.parse(minInterval.text);\n      int max = int.parse(maxInterval.text);\n      intervalValue = Random().nextInt(max - min) + min;\n    }\n\n    Future.delayed(Duration(milliseconds: intervalValue), () {\n      if (counter - 1 > 0) {\n        submitTask(counter - 1);\n      }\n    });\n  }\n\n  Future<void> _pickScheduleDateTime() async {\n    DateTime now = DateTime.now();\n\n    // Normalize minimum date to minute precision to avoid millisecond/second mismatches\n    DateTime minDate = DateTime(now.year, now.month, now.day, now.hour, now.minute);\n    DateTime initial = time ?? minDate;\n    if (initial.isBefore(minDate)) initial = minDate;\n\n    DateTime temp = initial;\n\n    var date = await showDialog<DateTime>(\n        context: context,\n        builder: (BuildContext context) {\n          return AlertDialog(\n            contentPadding: const EdgeInsets.all(16.0),\n            content: SizedBox(\n              height: 250,\n              width: 300,\n              child: CupertinoTheme(\n                data: CupertinoThemeData(brightness: Theme.of(context).brightness),\n                child: CupertinoDatePicker(\n                  mode: CupertinoDatePickerMode.dateAndTime,\n                  initialDateTime: initial,\n                  minimumDate: minDate,\n                  maximumDate: minDate.add(const Duration(days: 365)),\n                  use24hFormat: true,\n                  onDateTimeChanged: (val) {\n                    temp = val;\n                  },\n                ),\n              ),\n            ),\n            actions: [\n              TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n              TextButton(onPressed: () => Navigator.pop(context, temp), child: Text(localizations.done)),\n            ],\n          );\n        });\n\n    if (date != null) {\n      setState(() {\n        // ensure selected date is not before now (safety)\n        DateTime now2 = DateTime.now();\n        if (date.isBefore(now2)) {\n          // clamp to now to avoid scheduling into the past\n          time = now2;\n        } else {\n          time = date;\n        }\n      });\n    }\n  }\n\n  Widget field(String label, Widget child) {\n    return Row(\n      children: [\n        SizedBox(width: isEN ? 110 : 95, child: Text(label)),\n        Expanded(child: child),\n      ],\n    );\n  }\n\n  Widget textField(TextEditingController? controller, {TextStyle? style}) {\n    Color color = Theme.of(context).colorScheme.primary;\n\n    return ConstrainedBox(\n        constraints: const BoxConstraints(maxHeight: 42),\n        child: TextFormField(\n          controller: controller,\n          keyboardType: TextInputType.number,\n          inputFormatters: [FilteringTextInputFormatter.digitsOnly],\n          style: style,\n          decoration: InputDecoration(\n              errorStyle: const TextStyle(height: 2, fontSize: 0),\n              contentPadding: const EdgeInsets.only(left: 10, right: 10, top: 5, bottom: 5),\n              border: OutlineInputBorder(borderSide: BorderSide(width: 1, color: color.withAlpha((0.3 * 255).round()))),\n              enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.5, color: color.withAlpha((0.5 * 255).round()))),\n              focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color))),\n          validator: (val) => val == null || val.isEmpty ? localizations.cannotBeEmpty : null,\n        ));\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/request/report_servers.dart",
    "content": "/*\n * 上报服务器配置页面\n */\nimport 'package:flutter/material.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/components/manager/report_server_manager.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\nimport '../../../l10n/app_localizations.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\n// 以弹框的方式展示上报服务器管理\nFuture<void> showReportServersDialog(BuildContext context) {\n  return showDialog(\n    context: context,\n    barrierDismissible: false,\n    builder: (ctx) => Dialog(\n      insetPadding: const EdgeInsets.all(16),\n      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),\n      clipBehavior: Clip.antiAlias,\n      child: SizedBox(\n        width: 570,\n        height: 560,\n        child: const ReportServersPage(),\n      ),\n    ),\n  );\n}\n\nclass ReportServersPage extends StatefulWidget {\n  const ReportServersPage({super.key});\n\n  @override\n  State<ReportServersPage> createState() => _ReportServersPageState();\n}\n\nclass _ReportServersPageState extends State<ReportServersPage> {\n  List<ReportServer> _servers = [];\n  bool _loading = true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  Future<void> _openGuide() async {\n    final locale = AppLocalizations.of(context)?.localeName ?? '';\n    final cn = 'https://gitee.com/wanghongenpin/proxypin/wikis/%E4%B8%8A%E6%8A%A5%E6%9C%8D%E5%8A%A1%E5%99%A8';\n    final en = 'https://github.com/wanghongenpin/proxypin/wiki/Report-Server';\n    final url = (locale.startsWith('zh')) ? cn : en;\n    final uri = Uri.parse(url);\n    try {\n      if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {\n        FlutterToastr.show('Open guide failed', context);\n      }\n    } catch (e) {\n      FlutterToastr.show('Open guide failed: $e', context);\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _load();\n  }\n\n  Future<void> _load() async {\n    final manager = await ReportServerManager.instance;\n    final list = manager.servers;\n    setState(() {\n      _servers = List.of(list);\n      _loading = false;\n    });\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n\n  // 统一的新增/编辑弹窗\n  Future<ReportServer?> _showServerDialog({ReportServer? initial}) async {\n    final nameCtrl = TextEditingController(text: initial?.name ?? '');\n    final matchUrlCtrl = TextEditingController(text: initial?.matchUrl ?? '');\n    final serverUrlCtrl = TextEditingController(text: initial?.serverUrl ?? '');\n    String compression = initial?.compression ?? 'none';\n    bool enabled = initial?.enabled ?? true;\n\n    // 紧凑的 Outline 输入框装饰\n    InputDecoration dec({String? hint}) => InputDecoration(\n        hintText: hint,\n        hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n        contentPadding: const EdgeInsets.symmetric(horizontal: 5, vertical: 12),\n        errorStyle: const TextStyle(height: 0, fontSize: 0),\n        focusedBorder: focusedBorder(),\n        isDense: true,\n        border: const OutlineInputBorder());\n\n    Widget labeled(String label, Widget field, {bool expanded = true}) => Row(\n          children: [\n            SizedBox(width: AppLocalizations.of(context)!.localeName == 'en' ? 95 : 85, child: Text(label)),\n            const SizedBox(width: 12),\n            expanded ? Expanded(child: field) : field,\n          ],\n        );\n\n    final formKey = GlobalKey<FormState>();\n\n    final result = await showDialog<ReportServer>(\n      context: context,\n      builder: (ctx) {\n        return AlertDialog(\n          title: Text(initial == null ? localizations.addReportServer : localizations.editReportServer,\n              style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),\n          content: Form(\n              key: formKey,\n              child: SizedBox(\n                width: 460,\n                child: SingleChildScrollView(\n                  child: Column(\n                    mainAxisSize: MainAxisSize.min,\n                    children: [\n                      labeled(\n                        '${localizations.name}: ',\n                        TextField(controller: nameCtrl, decoration: dec(hint: localizations.pleaseEnter)),\n                      ),\n                      const SizedBox(height: 12),\n                      labeled(\n                        '${localizations.match} URL: ',\n                        TextFormField(\n                            controller: matchUrlCtrl,\n                            keyboardType: TextInputType.url,\n                            validator: (val) => val?.isNotEmpty == true ? null : \"\",\n                            decoration: dec(hint: 'https://example.com/api/*')),\n                      ),\n                      const SizedBox(height: 12),\n                      labeled(\n                        '${localizations.serverUrl}: ',\n                        TextFormField(\n                            controller: serverUrlCtrl,\n                            keyboardType: TextInputType.url,\n                            validator: (val) => val?.isNotEmpty == true ? null : \"\",\n                            decoration: dec(hint: 'http://example.com/report')),\n                      ),\n                      const SizedBox(height: 12),\n                      labeled(\n                          '${localizations.compression}: ',\n                          expanded: false,\n                          SizedBox(\n                            width: 100,\n                            child: DropdownButtonFormField<String>(\n                              value: compression,\n                              decoration: dec(),\n                              isDense: true,\n                              items: [\n                                DropdownMenuItem(value: 'none', child: Text(localizations.compressionNone)),\n                                DropdownMenuItem(value: 'gzip', child: Text(\"GZIP\")),\n                              ],\n                              onChanged: (v) => compression = v ?? 'none',\n                            ),\n                          )),\n                      const SizedBox(height: 12),\n                      labeled(\n                        '${localizations.enable}: ',\n                        Align(\n                          alignment: Alignment.centerLeft,\n                          child: SwitchWidget(value: enabled, scale: 0.83, onChanged: (v) => enabled = v),\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              )),\n          actions: [\n            TextButton(\n              onPressed: () => Navigator.pop(ctx, null),\n              child: Text(localizations.cancel),\n            ),\n            FilledButton(\n              onPressed: () {\n                if (!(formKey.currentState as FormState).validate()) {\n                  FlutterToastr.show(\"${localizations.serverUrl} ${localizations.cannotBeEmpty}\", context,\n                      position: FlutterToastr.top);\n                  return;\n                }\n\n                final matchUrl = matchUrlCtrl.text.trim();\n                var serverUrl = serverUrlCtrl.text.trim();\n                // 修复此前的前缀判断逻辑：仅当不以 http/https 开头时补全\n                if (!serverUrl.startsWith('http://') && !serverUrl.startsWith('https://')) {\n                  serverUrl = 'http://$serverUrl';\n                }\n\n                final server = ReportServer(\n                  name: nameCtrl.text.trim(),\n                  matchUrl: matchUrl,\n                  serverUrl: serverUrl,\n                  enabled: enabled,\n                  compression: compression,\n                );\n                Navigator.pop(ctx, server);\n              },\n              child: Text(localizations.save),\n            ),\n          ],\n        );\n      },\n    );\n\n    return result;\n  }\n\n  Future<void> _addServerDialog() async {\n    final server = await _showServerDialog();\n    if (server != null) {\n      final manager = await ReportServerManager.instance;\n      await manager.add(server);\n      await _load();\n    }\n  }\n\n  Future<void> _editServerDialog(int index) async {\n    final initial = _servers[index];\n    final server = await _showServerDialog(initial: initial);\n    if (server != null) {\n      final manager = await ReportServerManager.instance;\n      await manager.update(index, server);\n      setState(() => _servers[index] = server);\n    }\n  }\n\n  Future<void> _confirmDelete(int index) async {\n    showConfirmDialog(context, onConfirm: () async {\n      final manager = await ReportServerManager.instance;\n      await manager.removeAt(index);\n\n      await _load();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        automaticallyImplyLeading: false,\n        title: Text(localizations.reportServers),\n        centerTitle: true,\n        actions: [\n\n          TextButton.icon(\n            label: Text(localizations.newBuilt),\n            onPressed: _addServerDialog,\n            icon: const Icon(Icons.add),\n          ),\n          const SizedBox(width: 6),\n          IconButton(\n            tooltip: localizations.useGuide,\n            onPressed: _openGuide,\n            icon: const Icon(Icons.help_outline,size: 21),\n          ),\n          IconButton(\n            tooltip: localizations.close,\n            onPressed: () => Navigator.of(context).maybePop(),\n            icon: const Icon(Icons.close, size: 22),\n          ),\n          const SizedBox(width: 6),\n        ],\n      ),\n      body: _loading\n          ? const Center(child: CircularProgressIndicator())\n          : _servers.isEmpty\n              ? Center(child: Text(localizations.emptyData))\n              : Padding(\n                  padding: const EdgeInsets.all(8.0),\n                  child: SingleChildScrollView(\n                    scrollDirection: Axis.vertical,\n                    child: SingleChildScrollView(\n                      scrollDirection: Axis.horizontal,\n                      child: DataTable(\n                        headingRowHeight: 38,\n                        dataRowMinHeight: 40,\n                        dataRowMaxHeight: 48,\n                        horizontalMargin: 12,\n                        showBottomBorder: true,\n                        dividerThickness: 0.26,\n                        columnSpacing: 8,\n                        columns: [\n                          DataColumn(label: Center(child: Text(localizations.name))),\n                          DataColumn(label: Center(child: Text(localizations.enable))),\n                          DataColumn(label: Center(child: Text('${localizations.match} URL'))),\n                          DataColumn(label: Center(child: Text(localizations.serverUrl))),\n                          DataColumn(label: Center(child: Text(localizations.action))),\n                        ],\n                        rows: [\n                          for (final entry in _servers.asMap().entries)\n                            DataRow(cells: [\n                              DataCell(\n                                  SizedBox(\n                                      width: 65,\n                                      child: Text(\n                                        entry.value.name.isEmpty ? '-' : entry.value.name,\n                                        maxLines: 1,\n                                        overflow: TextOverflow.fade,\n                                      )),\n                                  onTap: () => _editServerDialog(entry.key)),\n                              DataCell(Center(\n                                  child: SizedBox(\n                                      width: 45,\n                                      child: SwitchWidget(\n                                        value: entry.value.enabled,\n                                        scale: 0.73,\n                                        onChanged: (v) async {\n                                          final manager = await ReportServerManager.instance;\n                                          await manager.toggleEnabled(entry.key, v);\n                                          setState(() => _servers[entry.key] = entry.value.copyWith(enabled: v));\n                                        },\n                                      )))),\n                              DataCell(\n                                  SizedBox(\n                                    width: 155,\n                                    child: Tooltip(\n                                      message: entry.value.matchUrl,\n                                      child: Text(entry.value.matchUrl, overflow: TextOverflow.ellipsis, maxLines: 1),\n                                    ),\n                                  ),\n                                  onTap: () => _editServerDialog(entry.key)),\n                              DataCell(\n                                  SizedBox(\n                                    width: 155,\n                                    child: Tooltip(\n                                      message: entry.value.serverUrl,\n                                      child: Text(entry.value.serverUrl, overflow: TextOverflow.ellipsis, maxLines: 1),\n                                    ),\n                                  ),\n                                  onTap: () => _editServerDialog(entry.key)),\n                              DataCell(Center(\n                                child: Row(\n                                  mainAxisSize: MainAxisSize.min,\n                                  children: [\n                                    IconButton(\n                                      tooltip: localizations.edit,\n                                      onPressed: () => _editServerDialog(entry.key),\n                                      icon: const Icon(Icons.edit_outlined, size: 18),\n                                    ),\n                                    IconButton(\n                                      tooltip: localizations.delete,\n                                      onPressed: () => _confirmDelete(entry.key),\n                                      icon: const Icon(Icons.delete_outline, size: 18),\n                                    ),\n                                  ],\n                                ),\n                              )),\n                            ])\n                        ],\n                      ),\n                    ),\n                  ),\n                ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/request/request.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:date_format/date_format.dart';\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_desktop_context_menu/flutter_desktop_context_menu.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/components/manager/script_manager.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/storage/favorites.dart';\nimport 'package:proxypin/ui/component/app_dialog.dart';\nimport 'package:proxypin/ui/component/multi_window.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/content/panel.dart';\nimport 'package:proxypin/ui/desktop/request/repeat.dart';\nimport 'package:proxypin/ui/desktop/setting/request_map.dart';\nimport 'package:proxypin/ui/desktop/setting/script.dart';\nimport 'package:proxypin/ui/desktop/widgets/highlight.dart';\nimport 'package:proxypin/utils/curl.dart';\nimport 'package:proxypin/utils/keyword_highlight.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/python.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\nimport 'package:window_manager/window_manager.dart';\n\nimport '../../../utils/export_request.dart';\nimport '../common.dart';\n\n/// 请求 URI\n/// @author wanghongen\n/// 2023/10/8\nclass RequestWidget extends StatefulWidget {\n  final int index;\n  final HttpRequest request;\n  final ValueWrap<HttpResponse> response = ValueWrap();\n  final bool displayDomain;\n\n  final ProxyServer proxyServer;\n  final Function(RequestWidget)? remove;\n  final Widget? trailing;\n\n  RequestWidget(this.request,\n      {Key? key, required this.proxyServer, this.remove, this.displayDomain = true, this.trailing, required this.index})\n      : super(key: GlobalKey<_RequestWidgetState>());\n\n  @override\n  State<RequestWidget> createState() => _RequestWidgetState();\n\n  void setResponse(HttpResponse response) {\n    this.response.set(response);\n    var state = key as GlobalKey<_RequestWidgetState>;\n    state.currentState?.changeState();\n  }\n}\n\nclass _RequestWidgetState extends State<RequestWidget> {\n  //选择的节点\n  static _RequestWidgetState? selectedState;\n\n  static Set<String> autoReadRequests = <String>{};\n\n  bool selected = false;\n\n  Color? highlightColor; //高亮颜色\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    autoReadRequests.remove(widget.request.requestId);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var request = widget.request;\n    var response = widget.response.get() ?? request.response;\n    String path = widget.displayDomain ? request.domainPath : request.path;\n    String title = '${request.method.name} $path';\n\n    var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]);\n    String contentType = response?.contentType.name.toUpperCase() ?? '';\n    var packagesSize = getPackagesSize(request, response);\n\n    var requestColor = color(path);\n\n    return GestureDetector(\n        onSecondaryTap: contextualMenu,\n        child: ListTile(\n            minLeadingWidth: 5,\n            textColor: requestColor,\n            selectedColor: requestColor,\n            selectedTileColor: Theme.of(context).colorScheme.primary.withOpacity(0.1),\n            leading: getIcon(widget.response.get() ?? widget.request.response, color: requestColor),\n            trailing: widget.trailing,\n            title: Text(title.fixAutoLines(), overflow: TextOverflow.ellipsis, maxLines: 2),\n            subtitle: Container(\n                padding: const EdgeInsets.only(top: 3),\n                child: Text.rich(\n                    maxLines: 1,\n                    TextSpan(\n                      children: [\n                        TextSpan(text: '#${widget.index} ', style: const TextStyle(fontSize: 11, color: Colors.teal)),\n                        TextSpan(\n                            text:\n                                '$time - [${response?.status.code ?? ''}]  $contentType $packagesSize ${response?.costTime() ?? ''}',\n                            style: const TextStyle(fontSize: 11, color: Colors.grey))\n                      ],\n                    ))),\n            selected: selected,\n            dense: true,\n            visualDensity: const VisualDensity(vertical: -4),\n            contentPadding: const EdgeInsets.only(left: 28),\n            onTap: onClick));\n  }\n\n  Color? color(String url) {\n    if (highlightColor != null) {\n      return highlightColor;\n    }\n\n    highlightColor = KeywordHighlights.getHighlightColor(url);\n    if (highlightColor != null) {\n      return highlightColor;\n    }\n\n    return autoReadRequests.contains(widget.request.requestId) ? Colors.grey : null;\n  }\n\n  void changeState() {\n    setState(() {});\n  }\n\n  void contextualMenu() {\n    Menu menu = Menu(items: [\n      MenuItem(\n          label: localizations.copyUrl,\n          onClick: (_) {\n            var requestUrl = widget.request.requestUrl;\n            Clipboard.setData(ClipboardData(text: requestUrl))\n                .then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context));\n          }),\n      MenuItem(\n          label: localizations.copy,\n          type: 'submenu',\n          submenu: Menu(items: [\n            MenuItem(\n                label: localizations.copyCurl,\n                onClick: (_) {\n                  Clipboard.setData(ClipboardData(text: curlRequest(widget.request)))\n                      .then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context));\n                }),\n            MenuItem(\n                label: localizations.copyRawRequest,\n                onClick: (_) {\n                  Clipboard.setData(ClipboardData(text: copyRawRequest(widget.request)))\n                      .then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context));\n                }),\n            MenuItem(\n                label: localizations.copyRequestResponse,\n                onClick: (_) {\n                  Clipboard.setData(ClipboardData(text: copyRequest(widget.request, widget.response.get())))\n                      .then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context));\n                }),\n            MenuItem(\n              label: localizations.copyAsPythonRequests,\n              onClick: (_) {\n                Clipboard.setData(ClipboardData(text: copyAsPythonRequests(widget.request)))\n                    .then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context));\n              },\n            ),\n          ])),\n      MenuItem.separator(),\n      MenuItem(\n          label: localizations.openNewWindow,\n          onClick: (_) {\n            openDetailInNewWindow();\n          }),\n      MenuItem.separator(),\n      MenuItem(\n          label: localizations.export,\n          type: 'submenu',\n          submenu: Menu(items: [\n            MenuItem(label: localizations.request, onClick: (_) => exportRequest(widget.request)),\n            MenuItem(label: localizations.requestBody, onClick: (_) => exportRequestBody(widget.request)),\n            MenuItem.separator(),\n            MenuItem(label: localizations.response, onClick: (_) => exportResponse(widget.response.get())),\n            MenuItem(label: localizations.responseBody, onClick: (_) => exportResponseBody(widget.response.get())),\n            MenuItem.separator(),\n            MenuItem(label: \"HAR\", onClick: (_) => exportHar(widget.request)),\n          ])),\n      MenuItem.separator(),\n      MenuItem(label: localizations.repeat, onClick: (_) => onRepeat(widget.request)),\n      MenuItem(label: localizations.customRepeat, onClick: (_) => showCustomRepeat(widget.request)),\n      MenuItem(\n          label: localizations.editRequest,\n          onClick: (_) {\n            WidgetsBinding.instance.addPostFrameCallback((_) {\n              requestEdit();\n            });\n          }),\n      MenuItem.separator(),\n      MenuItem(label: localizations.requestRewrite, onClick: (_) => showRequestRewriteDialog(context, widget.request)),\n      MenuItem(\n          label: localizations.requestMap,\n          onClick: (_) async {\n            showDialog(\n                context: context,\n                builder: (context) =>\n                    RequestMapEdit(url: widget.request.domainPath, title: widget.request.hostAndPort?.host));\n          }),\n      MenuItem(\n          label: localizations.script,\n          onClick: (_) async {\n            var scriptManager = await ScriptManager.instance;\n            var url = widget.request.domainPath;\n            var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.urls.contains(url));\n\n            String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem);\n            if (!mounted) return;\n            showDialog(\n                context: context,\n                builder: (context) => ScriptEdit(\n                    scriptItem: scriptItem, script: script, url: url, title: widget.request.hostAndPort?.host));\n          }),\n      MenuItem.separator(),\n      MenuItem(\n          label: localizations.favorite,\n          onClick: (_) {\n            FavoriteStorage.addFavorite(widget.request);\n            FlutterToastr.show(localizations.operationSuccess, context, rootNavigator: true);\n          }),\n      MenuItem(\n          label: localizations.highlight,\n          type: 'submenu',\n          submenu: highlightMenu(),\n          onClick: (_) {\n            setState(() {\n              highlightColor = Colors.red;\n            });\n          }),\n      MenuItem.separator(),\n      MenuItem(\n          label: localizations.delete,\n          onClick: (_) {\n            widget.remove?.call(widget);\n          }),\n    ]);\n\n    popUpContextMenu(menu);\n  }\n\n  ///高亮\n  Menu highlightMenu() {\n    return Menu(\n      items: [\n        MenuItem(\n            label: localizations.red,\n            onClick: (_) {\n              setState(() {\n                highlightColor = Colors.red;\n              });\n            }),\n        MenuItem(\n            label: localizations.yellow,\n            onClick: (_) {\n              setState(() {\n                highlightColor = Colors.yellow.shade600;\n              });\n            }),\n        MenuItem(\n            label: localizations.blue,\n            onClick: (_) {\n              setState(() {\n                highlightColor = Colors.blue;\n              });\n            }),\n        MenuItem(\n            label: localizations.green,\n            onClick: (_) {\n              setState(() {\n                highlightColor = Colors.green;\n              });\n            }),\n        MenuItem(\n            label: localizations.gray,\n            onClick: (_) {\n              setState(() {\n                highlightColor = Colors.grey;\n              });\n            }),\n        MenuItem.separator(),\n        MenuItem.checkbox(\n            label: localizations.autoRead,\n            checked: AppConfiguration.current?.autoReadEnabled,\n            onClick: (_) {\n              setState(() {\n                AppConfiguration.current?.autoReadEnabled = !AppConfiguration.current!.autoReadEnabled;\n              });\n            }),\n        MenuItem.separator(),\n        MenuItem(\n            label: localizations.reset,\n            onClick: (_) {\n              setState(() {\n                highlightColor = null;\n                autoReadRequests.clear();\n              });\n            }),\n        MenuItem(\n            label: localizations.keyword,\n            onClick: (_) {\n              showDialog(context: context, builder: (BuildContext context) => const DesktopKeywordHighlight());\n            }),\n      ],\n    );\n  }\n\n  //显示高级重发\n  Future<void> showCustomRepeat(HttpRequest request) async {\n    var prefs = await SharedPreferences.getInstance();\n    if (!mounted) return;\n\n    showDialog(\n        context: context,\n        builder: (BuildContext context) {\n          return CustomRepeatDialog(onRepeat: () => onRepeat(request), prefs: prefs);\n        });\n  }\n\n  void onRepeat(HttpRequest httpRequest) {\n    var request = httpRequest.copy(uri: httpRequest.requestUrl);\n    var proxyInfo = widget.proxyServer.isRunning ? ProxyInfo.of(\"127.0.0.1\", widget.proxyServer.port) : null;\n    HttpClients.proxyRequest(request, proxyInfo: proxyInfo);\n    FlutterToastr.show(localizations.reSendRequest, context, rootNavigator: true);\n  }\n\n  PopupMenuItem popupItem(String text, {VoidCallback? onTap}) {\n    return CustomPopupMenuItem(height: 32, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 13)));\n  }\n\n  ///请求编辑\n  Future<void> requestEdit() async {\n    var size = MediaQuery.of(context).size;\n    var ratio = 1.0;\n    if (Platform.isWindows) {\n      ratio = WindowManager.instance.getDevicePixelRatio();\n    }\n\n    final window = await DesktopMultiWindow.createWindow(jsonEncode(\n      {'name': 'RequestEditor', 'request': widget.request, 'proxyPort': widget.proxyServer.port},\n    ));\n\n    window.setTitle(localizations.requestEdit);\n    window\n      ..setFrame(const Offset(100, 100) & Size(960 * ratio, size.height * ratio))\n      ..center()\n      ..show();\n  }\n\n  // 新窗口打开详情\n  void openDetailInNewWindow() async {\n    MultiWindow.openWindow(\n      localizations.captureDetail,\n      'RequestDetailPage',\n      args: {\n        'request': widget.request,\n        'response': widget.request.response ?? widget.response.get(),\n      },\n      size: Size(850, 900),\n    );\n  }\n\n  //点击事件\n  void onClick() {\n    if (!selected) {\n      setState(() {\n        selected = true;\n      });\n    }\n\n    if (AppConfiguration.current?.autoReadEnabled == true) {\n      autoReadRequests.add(widget.request.requestId);\n    }\n\n    //切换选中的节点\n    if (selectedState?.mounted == true && selectedState != this) {\n      selectedState?.setState(() {\n        selectedState?.selected = false;\n      });\n    }\n\n    selectedState = this;\n    NetworkTabController.current?.change(widget.request, widget.response.get() ?? widget.request.response);\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/request/request_editor.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/split_view.dart';\nimport 'package:proxypin/ui/component/state_component.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/content/body.dart';\nimport 'package:proxypin/utils/curl.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nimport '../../component/http_method_popup.dart';\n\nenum RequestEditorSource {\n  editor,\n  breakpointRequest,\n  breakpointResponse,\n}\n\n/// @author wanghongen\nclass RequestEditor extends StatefulWidget {\n  final WindowController? windowController;\n  final HttpRequest? request;\n  final RequestEditorSource source;\n  final Function(HttpRequest? request)? onExecuteRequest;\n  final Function(HttpResponse? response)? onExecuteResponse;\n  final HttpResponse? response;\n\n  const RequestEditor({\n    super.key,\n    this.request,\n    this.response,\n    this.windowController,\n    this.source = RequestEditorSource.editor,\n    this.onExecuteRequest,\n    this.onExecuteResponse,\n  });\n\n  @override\n  State<StatefulWidget> createState() {\n    return RequestEditorState();\n  }\n}\n\nclass RequestEditorState extends State<RequestEditor> {\n  final UrlQueryNotifier _queryNotifier = UrlQueryNotifier();\n  final requestLineKey = GlobalKey<_RequestLineState>();\n  final requestKey = GlobalKey<_HttpState>();\n  final responseKey = GlobalKey<_HttpState>();\n\n  ValueNotifier<int> responseChange = ValueNotifier<int>(-1);\n  HttpRequest? request;\n  HttpResponse? response;\n\n  bool showCURLDialog = false;\n  bool executed = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    request = widget.request;\n    response = widget.response;\n    if (response != null) {\n      responseChange.value = 1;\n    }\n    HardwareKeyboard.instance.addHandler(onKeyEvent);\n    if (widget.request == null) {\n      curlParse();\n    }\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if ((HardwareKeyboard.instance.isMetaPressed ||\n            HardwareKeyboard.instance.isControlPressed ||\n            HardwareKeyboard.instance.isAltPressed) &&\n        event.logicalKey == LogicalKeyboardKey.enter) {\n      sendRequest();\n      return true;\n    }\n\n    //cmd+w 关闭窗口\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      responseChange.dispose();\n      widget.windowController?.close();\n      return true;\n    }\n\n    //粘贴\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyV) {\n      curlParse();\n      return true;\n    }\n\n    return false;\n  }\n\n  @override\n  void dispose() {\n    if ((widget.source == RequestEditorSource.breakpointRequest ||\n            widget.source == RequestEditorSource.breakpointResponse) &&\n        !executed) {\n      if (widget.source == RequestEditorSource.breakpointRequest) {\n        widget.onExecuteRequest?.call(null);\n      } else {\n        widget.onExecuteResponse?.call(null);\n      }\n    }\n\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    responseChange.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var title = localizations.httpRequest;\n    var buttonText = localizations.send;\n    IconData icon = Icons.send;\n\n    if (widget.source == RequestEditorSource.breakpointRequest) {\n      title = \"Breakpoint Request\";\n      buttonText = localizations.execute;\n      icon = Icons.play_arrow;\n    } else if (widget.source == RequestEditorSource.breakpointResponse) {\n      title = \"Breakpoint Response\";\n      buttonText = localizations.execute;\n      icon = Icons.play_arrow;\n    }\n\n    return Scaffold(\n        appBar: AppBar(\n          title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n          toolbarHeight: Platform.isWindows ? 36 : null,\n          centerTitle: true,\n          actions: [\n            TextButton.icon(\n                onPressed: () async {\n                  if (widget.source == RequestEditorSource.editor) {\n                    sendRequest();\n                  } else {\n                    executeBreakpoint();\n                  }\n                },\n                icon: Icon(icon),\n                label: Text(buttonText)),\n            if (widget.source == RequestEditorSource.breakpointRequest ||\n                widget.source == RequestEditorSource.breakpointResponse)\n              TextButton.icon(\n                  onPressed: () {\n                    // ignore breakpoint\n                    if (widget.source == RequestEditorSource.breakpointRequest) {\n                      widget.onExecuteRequest?.call(null);\n                    } else {\n                      widget.onExecuteResponse?.call(null);\n                    }\n                    widget.windowController?.close();\n                  },\n                  icon: const Icon(Icons.cancel),\n                  label: Text(localizations.cancel)),\n            const SizedBox(width: 10)\n          ],\n        ),\n        body: Column(children: [\n          _RequestLine(key: requestLineKey, request: request, urlQueryNotifier: _queryNotifier),\n          Expanded(\n              child: VerticalSplitView(\n            ratio: 0.53,\n            left: _HttpWidget(\n              key: requestKey,\n              title: const Text(\"Request\", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n              message: request,\n              urlQueryNotifier: _queryNotifier,\n              readOnly: widget.source == RequestEditorSource.breakpointResponse,\n            ),\n            right: ValueListenableBuilder(\n                valueListenable: responseChange,\n                builder: (_, value, __) {\n                  return Stack(\n                    children: [\n                      Offstage(offstage: value != 0, child: const Center(child: CircularProgressIndicator())),\n                      Offstage(\n                          offstage: value == 0,\n                          child: _HttpWidget(\n                              key: responseKey,\n                              title: Row(children: [\n                                const Text(\"Response\", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n                                const Spacer(),\n                                Text.rich(TextSpan(children: [\n                                  TextSpan(\n                                      text: response?.protocolVersion,\n                                      style: const TextStyle(\n                                          fontSize: 14,\n                                          fontWeight: FontWeight.w500,\n                                          decorationColor: Colors.green,\n                                          color: Colors.green)),\n                                  WidgetSpan(child: SizedBox(width: 12)),\n                                  TextSpan(\n                                      text: response?.status.code.toString() ?? '',\n                                      style: TextStyle(\n                                          fontWeight: FontWeight.w500,\n                                          fontSize: 14,\n                                          color: response?.status.isSuccessful() == true ? Colors.green : Colors.red))\n                                ]))\n                              ]),\n                              message: response,\n                              readOnly: widget.source != RequestEditorSource.breakpointResponse))\n                    ],\n                  );\n                }),\n          )),\n        ]));\n  }\n\n  ///发送请求\n  Future<void> sendRequest() async {\n    var currentState = requestLineKey.currentState!;\n    var headers = requestKey.currentState?.getHeaders();\n    var requestBody = requestKey.currentState?.getBody();\n    String url = currentState.requestUrl.text;\n    HttpRequest request = HttpRequest(currentState.requestMethod, Uri.parse(url).toString(),\n        protocolVersion: this.request?.protocolVersion ?? \"HTTP/1.1\");\n    request.headers.addAll(headers);\n    request.body = requestBody == null ? null : utf8.encode(requestBody);\n\n    responseKey.currentState?.change(null);\n    responseChange.value = 0;\n\n    Map? proxyResult = await DesktopMultiWindow.invokeMethod(0, 'getProxyInfo');\n    ProxyInfo? proxyInfo = proxyResult == null ? null : ProxyInfo.of(proxyResult['host'], proxyResult['port']);\n\n    HttpClients.proxyRequest(request, proxyInfo: proxyInfo, timeout: Duration(seconds: 15)).then((response) {\n      this.response = response;\n      responseKey.currentState?.change(response);\n      responseChange.value = 1;\n      // if (mounted) FlutterToastr.show(localizations.requestSuccess, context);\n    }).catchError((e, stackTrace) {\n      logger.e(\"Request failed\", error: e, stackTrace: stackTrace);\n      responseChange.value = -1;\n      if (mounted) FlutterToastr.show('${localizations.fail}$e', context);\n    });\n  }\n\n  void executeBreakpoint() {\n    executed = true;\n    if (widget.source == RequestEditorSource.breakpointRequest) {\n      var currentState = requestLineKey.currentState!;\n      var headers = requestKey.currentState?.getHeaders();\n      var requestBody = requestKey.currentState?.getBody();\n      String url = currentState.requestUrl.text;\n\n      if (request == null) return;\n      HttpRequest newRequest = request!.copy(uri: url);\n      newRequest.method = currentState.requestMethod;\n      newRequest.headers.clear();\n      newRequest.headers.addAll(headers);\n      newRequest.body = requestBody == null ? null : utf8.encode(requestBody);\n      widget.onExecuteRequest?.call(newRequest);\n    } else if (widget.source == RequestEditorSource.breakpointResponse) {\n      var headers = responseKey.currentState?.getHeaders();\n      var responseBody = responseKey.currentState?.getBody();\n\n      if (response == null) return;\n      HttpResponse newResponse = response!.copy();\n      newResponse.headers.clear();\n      newResponse.headers.addAll(headers);\n      newResponse.body = responseBody == null ? null : utf8.encode(responseBody);\n      widget.onExecuteResponse?.call(newResponse);\n    }\n  }\n\n  Future<void> curlParse() async {\n    var data = await Clipboard.getData('text/plain');\n    if (data == null || data.text == null) {\n      return;\n    }\n\n    var text = data.text;\n    if (text?.startsWith(\"http://\") == true || text?.startsWith(\"https://\") == true) {\n      requestLineKey.currentState?.requestUrl.text = text!;\n      return;\n    }\n\n    if (text?.trimLeft().startsWith('curl') == true && mounted && !showCURLDialog) {\n      showCURLDialog = true;\n      showDialog(\n        context: context,\n        builder: (BuildContext context) {\n          return AlertDialog(\n              title: Text(localizations.prompt),\n              content: Text(localizations.curlSchemeRequest),\n              actions: [\n                TextButton(child: Text(localizations.cancel), onPressed: () => Navigator.of(context).pop()),\n                TextButton(\n                    child: Text(localizations.confirm),\n                    onPressed: () {\n                      try {\n                        setState(() {\n                          request = Curl.parse(text!);\n                          requestKey.currentState?.change(request!);\n                          requestLineKey.currentState?.change(request?.requestUrl, request?.method);\n                        });\n                      } catch (e) {\n                        FlutterToastr.show(localizations.fail, context);\n                      }\n                      Navigator.of(context).pop();\n                    }),\n              ]);\n        },\n      ).then((value) => showCURLDialog = false);\n    }\n  }\n}\n\ntypedef ParamCallback = void Function(String param);\n\nclass UrlQueryNotifier {\n  ParamCallback? _urlNotifier;\n  ParamCallback? _paramNotifier;\n\n  ParamCallback urlListener(ParamCallback listener) => _urlNotifier = listener;\n\n  ParamCallback paramListener(ParamCallback listener) => _paramNotifier = listener;\n\n  void onUrlChange(String url) => _urlNotifier?.call(url);\n\n  void onParamChange(String param) => _paramNotifier?.call(param);\n}\n\nclass _HttpWidget extends StatefulWidget {\n  final HttpMessage? message;\n  final bool readOnly;\n  final Widget title;\n  final UrlQueryNotifier? urlQueryNotifier;\n\n  const _HttpWidget({this.message, this.readOnly = false, super.key, required this.title, this.urlQueryNotifier});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _HttpState();\n  }\n}\n\nclass _HttpState extends State<_HttpWidget> {\n  List<String> tabs = ['Header', 'Body'];\n  final headerKey = GlobalKey<KeyValState>();\n  Map<String, List<String>> initHeader = {};\n  HttpMessage? message;\n  TextEditingController? body;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  String? getBody() {\n    return body?.text;\n  }\n\n  HttpHeaders? getHeaders() {\n    return HttpHeaders.fromJson(headerKey.currentState?.getParams() ?? {});\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.urlQueryNotifier != null) {\n      tabs.insert(0, \"URL Params\");\n    }\n\n    message = widget.message;\n    body = TextEditingController(text: widget.message?.bodyAsString);\n    if (widget.message?.headers == null && !widget.readOnly) {\n      initHeader[\"User-Agent\"] = [\"ProxyPin/${AppConfiguration.version}\"];\n      initHeader[\"Accept\"] = [\"*/*\"];\n      return;\n    }\n  }\n\n  void change(HttpMessage? message) {\n    this.message = message;\n    body?.text = message?.bodyAsString ?? '';\n    headerKey.currentState?.refreshParam(message?.headers.getHeaders());\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.message == null && widget.readOnly) {\n      return Scaffold(appBar: AppBar(title: widget.title), body: Center(child: Text(localizations.emptyData)));\n    }\n\n    return SingleChildScrollView(\n        child: SizedBox(\n            height: MediaQuery.of(context).size.height - 120,\n            child: DefaultTabController(\n                length: tabs.length,\n                initialIndex: tabs.length >= 3 ? 1 : 0,\n                child: Scaffold(\n                  primary: false,\n                  appBar: PreferredSize(\n                      preferredSize: const Size.fromHeight(70),\n                      child: AppBar(\n                        title: widget.title,\n                        bottom: TabBar(tabs: tabs.map((e) => Tab(text: e, height: 35)).toList()),\n                      )),\n                  body: Padding(\n                      padding: const EdgeInsets.only(left: 10),\n                      child: TabBarView(\n                        children: [\n                          if (tabs.length == 3)\n                            KeyValWidget(\n                                paramNotifier: widget.urlQueryNotifier,\n                                params: message is HttpRequest\n                                    ? (message as HttpRequest).requestUri?.queryParametersAll\n                                    : null),\n                          KeyValWidget(\n                              key: headerKey,\n                              params: message?.headers.getHeaders() ?? initHeader,\n                              readOnly: widget.readOnly,\n                              suggestions: HttpHeaders.commonHeaderKeys),\n                          _body()\n                        ],\n                      )),\n                ))));\n  }\n\n  Widget _body() {\n    if (widget.readOnly) {\n      return KeepAliveWrapper(\n          child: SingleChildScrollView(child: HttpBodyWidget(httpMessage: message, hideRequestRewrite: true)));\n    }\n\n    return TextFormField(autofocus: true, controller: body, readOnly: widget.readOnly, minLines: 20, maxLines: 20);\n  }\n}\n\n///请求行\nclass _RequestLine extends StatefulWidget {\n  final HttpRequest? request;\n  final UrlQueryNotifier? urlQueryNotifier;\n\n  const _RequestLine({super.key, this.request, this.urlQueryNotifier});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _RequestLineState();\n  }\n}\n\nclass _RequestLineState extends State<_RequestLine> {\n  HttpMethod requestMethod = HttpMethod.get;\n  TextEditingController requestUrl = TextEditingController(text: \"\");\n\n  @override\n  void initState() {\n    super.initState();\n    widget.urlQueryNotifier?.paramListener((param) => onQueryChange(param));\n    if (widget.request == null) {\n      requestUrl.text = 'https://';\n      return;\n    }\n\n    var request = widget.request!;\n    requestUrl.text = request.requestUrl;\n    requestMethod = request.method;\n  }\n\n  @override\n  dispose() {\n    requestUrl.dispose();\n    super.dispose();\n  }\n\n  void change(String? requestUrl, HttpMethod? requestMethod) {\n    this.requestUrl.text = requestUrl ?? this.requestUrl.text;\n    this.requestMethod = requestMethod ?? this.requestMethod;\n\n    urlNotifier();\n  }\n\n  void urlNotifier() {\n    var splitFirst = requestUrl.text.splitFirst(\"?\".codeUnits.first);\n    widget.urlQueryNotifier?.onUrlChange(splitFirst.length > 1 ? splitFirst.last : '');\n  }\n\n  void onQueryChange(String query) {\n    var url = requestUrl.text;\n    var indexOf = url.indexOf(\"?\");\n    if (indexOf == -1) {\n      requestUrl.text = \"$url?$query\";\n    } else {\n      requestUrl.text = \"${url.substring(0, indexOf)}?$query\";\n    }\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TextField(\n        controller: requestUrl,\n        decoration: InputDecoration(\n            prefix: Padding(\n              padding: const EdgeInsets.only(right: 6),\n              child: MethodPopupMenu(\n                value: requestMethod,\n                showSeparator: true,\n                onChanged: (val) {\n                  setState(() => requestMethod = val!);\n                },\n              ),\n            ),\n            isDense: true,\n            border: const OutlineInputBorder(borderSide: BorderSide()),\n            enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.grey, width: 0.3))),\n        onChanged: (value) {\n          urlNotifier();\n        });\n  }\n}\n\nclass KeyVal {\n  bool enabled = true;\n  TextEditingController key;\n  TextEditingController value;\n  FocusNode? keyFocusNode;\n  FocusNode? valueFocusNode;\n\n  KeyVal(this.key, this.value);\n}\n\n///key value\nclass KeyValWidget extends StatefulWidget {\n  final Map<String, List<String>>? params;\n  final bool readOnly; //只读\n  final UrlQueryNotifier? paramNotifier;\n  final List<String>? suggestions;\n\n  const KeyValWidget({super.key, this.params, this.readOnly = false, this.paramNotifier, this.suggestions});\n\n  @override\n  State<StatefulWidget> createState() => KeyValState();\n}\n\nclass KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin {\n  final List<KeyVal> _params = [];\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    widget.paramNotifier?.urlListener((url) => onChange(url));\n    if (widget.params == null) {\n      var keyVal = KeyVal(TextEditingController(), TextEditingController());\n      _params.add(keyVal);\n      return;\n    }\n\n    widget.params?.forEach((name, values) {\n      for (var val in values) {\n        var keyVal = KeyVal(TextEditingController(text: name), TextEditingController(text: val));\n        _params.add(keyVal);\n      }\n    });\n  }\n\n  @override\n  dispose() {\n    clear();\n    super.dispose();\n  }\n\n  //监听url发生变化 更改表单\n  void onChange(String value) {\n    var query = value.split(\"&\");\n    int index = 0;\n    while (index < query.length) {\n      var splitFirst = query[index].splitFirst('='.codeUnits.first);\n      String key = splitFirst.first;\n      String? val = splitFirst.length == 1 ? null : splitFirst.last;\n      if (_params.length <= index) {\n        _params.add(KeyVal(TextEditingController(text: key), TextEditingController(text: val)));\n        continue;\n      }\n\n      var keyVal = _params[index++];\n      keyVal.key.text = key;\n      keyVal.value.text = val ?? '';\n    }\n\n    _params.length = index;\n    setState(() {});\n  }\n\n  void notifierChange() {\n    if (widget.paramNotifier == null) return;\n    String query = _params\n        .where((e) => e.enabled && e.key.text.isNotEmpty)\n        .map((e) => \"${e.key.text}=${e.value.text}\".replaceAll(\"&\", \"%26\"))\n        .join(\"&\");\n    widget.paramNotifier?.onParamChange(query);\n  }\n\n  void clear() {\n    for (var element in _params) {\n      element.key.dispose();\n      element.value.dispose();\n    }\n    _params.clear();\n  }\n\n  //刷新param\n  void refreshParam(Map<String, List<String>>? headers) {\n    clear();\n    setState(() {\n      headers?.forEach((name, values) {\n        for (var val in values) {\n          var keyVal = KeyVal(TextEditingController(text: name), TextEditingController(text: val));\n          _params.add(keyVal);\n        }\n      });\n    });\n  }\n\n  ///获取所有请求头\n  Map<String, List<String>> getParams() {\n    Map<String, List<String>> map = {};\n    for (var keVal in _params) {\n      if (keVal.key.text.isEmpty || !keVal.enabled) {\n        continue;\n      }\n      map[keVal.key.text] ??= [];\n      map[keVal.key.text]!.add(keVal.value.text);\n    }\n\n    return map;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    var list = [\n      const Row(children: [\n        SizedBox(width: 38),\n        Expanded(flex: 4, child: Text('Key')),\n        Expanded(flex: 5, child: Text('Value'))\n      ]),\n      ..._buildRows(),\n    ];\n\n    if (!widget.readOnly) {\n      list.add(TextButton(\n        child: Text(localizations.add, textAlign: TextAlign.center),\n        onPressed: () {\n          setState(() {\n            _params.add(KeyVal(TextEditingController(), TextEditingController()));\n          });\n        },\n      ));\n    }\n    return Scaffold(\n        body: Padding(\n            padding: const EdgeInsets.only(top: 10),\n            child: ListView.separated(\n                separatorBuilder: (context, index) =>\n                    index == list.length ? const SizedBox() : const Divider(thickness: 0.2),\n                itemBuilder: (context, index) => list[index],\n                itemCount: list.length)));\n  }\n\n  List<Widget> _buildRows() {\n    List<Widget> list = [];\n    for (var keyVal in _params) {\n      list.add(_row(\n          keyVal,\n          widget.readOnly\n              ? null\n              : Padding(\n                  padding: const EdgeInsets.only(right: 15),\n                  child: InkWell(\n                      onTap: () {\n                        setState(() {\n                          _params.remove(keyVal);\n                          keyVal.key.dispose();\n                          keyVal.value.dispose();\n                        });\n                        notifierChange();\n                      },\n                      child: const Icon(Icons.remove_circle, size: 16)))));\n    }\n\n    return list;\n  }\n\n  Widget _cell(KeyVal keyVal,\n      {bool isKey = false,\n      FocusNode? focusNode,\n      List<String>? suggestions,\n      Map<String, List<String>>? valueSuggestions}) {\n    TextEditingController textController = isKey ? keyVal.key : keyVal.value;\n\n    if (!widget.readOnly && (suggestions != null || valueSuggestions != null)) {\n      return Container(\n          padding: const EdgeInsets.only(right: 5),\n          child: RawAutocomplete<String>(\n            textEditingController: textController,\n            focusNode: focusNode,\n            optionsBuilder: (TextEditingValue textEditingValue) {\n              if (textEditingValue.text.isEmpty) {\n                return const Iterable<String>.empty();\n              }\n\n              var currentSuggestions = suggestions;\n              if (!isKey && valueSuggestions?.containsKey(keyVal.key.text) == true) {\n                currentSuggestions = valueSuggestions![keyVal.key.text];\n              }\n\n              if (currentSuggestions == null) {\n                return const Iterable<String>.empty();\n              }\n\n              return currentSuggestions.where((String option) {\n                return option.toLowerCase().contains(textEditingValue.text.toLowerCase());\n              });\n            },\n            onSelected: (String selection) {\n              textController.text = selection;\n              notifierChange();\n            },\n            fieldViewBuilder: (BuildContext context, TextEditingController textEditingController,\n                FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) {\n              return TextFormField(\n                  controller: textEditingController,\n                  focusNode: fieldFocusNode,\n                  onFieldSubmitted: (String value) {\n                    onFieldSubmitted();\n                  },\n                  onChanged: (val) {\n                    if (isKey) setState(() {});\n                    notifierChange();\n                  },\n                  style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),\n                  minLines: 1,\n                  maxLines: 3,\n                  decoration: InputDecoration(\n                      isDense: true,\n                      hintStyle: const TextStyle(color: Colors.grey),\n                      contentPadding: const EdgeInsets.fromLTRB(5, 13, 5, 13),\n                      focusedBorder: OutlineInputBorder(\n                          borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 1.5)),\n                      border: InputBorder.none,\n                      hintText: isKey ? \"Key\" : \"Value\"));\n            },\n            optionsViewBuilder:\n                (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {\n              return Align(\n                alignment: Alignment.topLeft,\n                child: Material(\n                  elevation: 4.0,\n                  child: ConstrainedBox(\n                    constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300),\n                    child: ListView.builder(\n                      padding: EdgeInsets.zero,\n                      shrinkWrap: true,\n                      itemCount: options.length,\n                      itemBuilder: (BuildContext context, int index) {\n                        final String option = options.elementAt(index);\n                        return InkWell(\n                          onTap: () {\n                            onSelected(option);\n                          },\n                          child: Container(\n                            padding: const EdgeInsets.all(10.0),\n                            child: _buildHighlightText(option, textController.text),\n                          ),\n                        );\n                      },\n                    ),\n                  ),\n                ),\n              );\n            },\n          ));\n    }\n\n    return Container(\n        padding: const EdgeInsets.only(right: 5),\n        child: TextFormField(\n            readOnly: widget.readOnly,\n            style: TextStyle(fontSize: 13, fontWeight: isKey ? FontWeight.w500 : null),\n            controller: textController,\n            onChanged: (val) => notifierChange(),\n            minLines: 1,\n            maxLines: 3,\n            decoration: InputDecoration(\n                isDense: true,\n                hintStyle: const TextStyle(color: Colors.grey),\n                contentPadding: const EdgeInsets.fromLTRB(5, 13, 5, 13),\n                focusedBorder: widget.readOnly\n                    ? null\n                    : OutlineInputBorder(\n                        borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 1.5)),\n                border: InputBorder.none,\n                hintText: isKey ? \"Key\" : \"Value\")));\n  }\n\n  Widget _row(KeyVal keyVal, Widget? op) {\n    if (widget.suggestions != null) {\n      keyVal.keyFocusNode ??= FocusNode();\n    }\n\n    Map<String, List<String>>? valueSuggestions;\n    if (widget.suggestions != null) {\n      keyVal.valueFocusNode ??= FocusNode();\n      valueSuggestions = HttpHeaders.commonHeaderValues;\n    }\n\n    return Row(crossAxisAlignment: CrossAxisAlignment.center, children: [\n      if (op != null)\n        Checkbox(\n            value: keyVal.enabled,\n            onChanged: (val) {\n              setState(() {\n                keyVal.enabled = val!;\n              });\n              notifierChange();\n            }),\n      Container(width: 5),\n      Expanded(\n          flex: 4, child: _cell(keyVal, isKey: true, suggestions: widget.suggestions, focusNode: keyVal.keyFocusNode)),\n      const Text(\":\", style: TextStyle(color: Colors.deepOrangeAccent)),\n      const SizedBox(width: 8),\n      Expanded(flex: 6, child: _cell(keyVal, focusNode: keyVal.valueFocusNode, valueSuggestions: valueSuggestions)),\n      op ?? const SizedBox()\n    ]);\n  }\n\n  Widget _buildHighlightText(String text, String query) {\n    if (query.isEmpty) {\n      return Text(text);\n    }\n\n    int index = text.toLowerCase().indexOf(query.toLowerCase());\n    if (index < 0) {\n      return Text(text);\n    }\n\n    return Text.rich(TextSpan(children: [\n      TextSpan(text: text.substring(0, index)),\n      TextSpan(\n          text: text.substring(index, index + query.length),\n          style: TextStyle(color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold)),\n      TextSpan(text: text.substring(index + query.length))\n    ]));\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/request/request_sequence.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:collection';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/desktop/request/request.dart';\nimport 'package:proxypin/utils/keyword_highlight.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\n\nimport '../../component/model/search_model.dart';\n\n///请求序列 列表\n/// @author wanghongen\nclass RequestSequence extends StatefulWidget {\n  final ListenableList<HttpRequest> container;\n  final ProxyServer proxyServer;\n  final bool displayDomain;\n  final Function(List<HttpRequest>)? onRemove;\n\n  const RequestSequence(\n      {super.key, required this.container, required this.proxyServer, this.displayDomain = true, this.onRemove});\n\n  @override\n  State<StatefulWidget> createState() {\n    return RequestSequenceState();\n  }\n}\n\nclass RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliveClientMixin {\n  late Configuration configuration;\n\n  ///显示的请求列表 最新的在前面\n  Queue<HttpRequest> view = Queue();\n  bool changing = false;\n\n  bool sortDesc = true;\n\n  //搜索的内容\n  SearchModel? searchModel;\n\n  //关键词高亮监听\n  late VoidCallback highlightListener;\n\n  @override\n  void initState() {\n    super.initState();\n    configuration = widget.proxyServer.configuration;\n    view.addAll(widget.container.source.reversed);\n\n    highlightListener = () {\n      //回调时机在高亮设置页面dispose之后。所以需要在下一帧刷新，否则会报错\n      WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n        highlightHandler();\n      });\n    };\n    KeywordHighlights.addListener(highlightListener);\n  }\n\n  changeState() {\n    //防止频繁刷新\n    if (!changing) {\n      changing = true;\n      Future.delayed(const Duration(milliseconds: 500), () {\n        setState(() {\n          changing = false;\n        });\n      });\n    }\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void dispose() {\n    KeywordHighlights.removeListener(highlightListener);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return ListView.separated(\n        cacheExtent: 1000,\n        separatorBuilder: (context, index) => Divider(thickness: 0.2, height: 0, color: Theme.of(context).dividerColor),\n        itemCount: view.length,\n        itemBuilder: (context, index) {\n          return RequestWidget(\n            key: ValueKey(view.elementAt(index).requestId),\n            view.elementAt(index),\n            index: sortDesc ? view.length - index : index,\n            trailing: appIcon(view.elementAt(index)),\n            proxyServer: widget.proxyServer,\n            displayDomain: widget.displayDomain,\n            remove: (requestWidget) {\n              setState(() {\n                view.remove(requestWidget.request);\n              });\n              widget.onRemove?.call([requestWidget.request]);\n            },\n          );\n        });\n  }\n\n  Widget? appIcon(HttpRequest request) {\n    var processInfo = request.processInfo;\n    if (processInfo == null) {\n      return null;\n    }\n\n    return futureWidget(\n        processInfo.getIcon(),\n        (data) => data.isEmpty\n            ? const SizedBox()\n            : Image.memory(\n                data,\n                width: 23,\n                height: Platform.isWindows ? 16 : null,\n                errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) => const SizedBox(),\n              ));\n  }\n\n  ///高亮处理\n  void highlightHandler() {\n    setState(() {});\n  }\n\n  ///添加请求\n  void add(HttpRequest request) {\n    ///过滤\n    if (searchModel?.isNotEmpty == true && !searchModel!.filter(request, request.response)) {\n      return;\n    }\n\n    if (sortDesc) {\n      view.addFirst(request);\n    } else {\n      view.addLast(request);\n    }\n\n    changeState();\n  }\n\n  ///添加响应\n  void addResponse(HttpResponse response) {\n    if (searchModel == null || searchModel!.isEmpty || response.request == null) {\n      changeState();\n      return;\n    }\n\n    //搜索视图\n    if (searchModel?.filter(response.request!, response) == true) {\n      if (!view.contains(response.request)) {\n        view.addFirst(response.request!);\n        changeState();\n      }\n    }\n  }\n\n  ///过滤\n  void search(SearchModel searchModel) {\n    this.searchModel = searchModel;\n    if (searchModel.isEmpty) {\n      view = Queue.of(widget.container.source.reversed);\n    } else {\n      view = Queue.of(widget.container.where((it) => searchModel.filter(it, it.response)).toList().reversed);\n    }\n    setState(() {});\n  }\n\n  void remove(List<HttpRequest> list) {\n    setState(() {\n      view.removeWhere((element) => list.contains(element));\n    });\n  }\n\n  void clean() {\n    setState(() {\n      view.clear();\n      view.addAll(widget.container.source.reversed);\n    });\n  }\n\n  ///排序\n  void sort(bool desc) {\n    sortDesc = desc;\n    setState(() {\n      view = Queue.of(view.toList().reversed);\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/request/search.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/http/content_type.dart';\nimport 'package:proxypin/ui/component/search_condition.dart';\n\nimport '../../component/model/search_model.dart';\n\n/// @author wanghongen\n/// 2023/10/8\nclass Search extends StatefulWidget {\n  final Function(SearchModel searchModel)? onSearch;\n\n  const Search({super.key, this.onSearch});\n\n  @override\n  State<StatefulWidget> createState() {\n    return SearchState();\n  }\n}\n\nclass SearchState extends State<Search> {\n  SearchModel searchModel = SearchModel();\n  bool searched = false;\n  TextEditingController keywordController = TextEditingController();\n  bool changing = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 32,\n      width: 300,\n      decoration: BoxDecoration(\n        color: Theme.of(context).hoverColor,\n        borderRadius: BorderRadius.circular(20),\n        boxShadow: [\n          BoxShadow(\n            color: Theme.of(context).shadowColor.withValues(alpha: 0.05),\n            blurRadius: 6,\n            offset: const Offset(0, 1),\n          )\n        ],\n      ),\n      child: TextField(\n        cursorHeight: 22,\n        controller: keywordController,\n        onChanged: (val) async {\n          searchModel.keyword = val;\n\n          if (!changing) {\n            changing = true;\n            Future.delayed(const Duration(milliseconds: 500), () {\n              changing = false;\n              if (!searched) {\n                searchModel.searchOptions = {Option.url, Option.method, Option.responseContentType};\n              }\n              widget.onSearch?.call(searchModel);\n            });\n          }\n        },\n        decoration: InputDecoration(\n          contentPadding: const EdgeInsets.all(0),\n          enabledBorder: OutlineInputBorder(\n              borderSide: BorderSide(color: Colors.grey.shade400, width: 0.5), borderRadius: BorderRadius.circular(15)),\n          border: OutlineInputBorder(\n              borderSide: BorderSide(color: Colors.grey.shade400, width: 0.5), borderRadius: BorderRadius.circular(15)),\n          prefixIcon: InkWell(\n              child: Icon(Icons.search, color: searched ? Colors.green : Theme.of(context).colorScheme.primary),\n              onTapDown: (details) {\n                searchDialog(details);\n              }),\n          hintText: 'Search',\n          suffixIcon: ContentTypeSelect(onSelected: (contentType) {\n            searchModel.responseContentType = contentType;\n            widget.onSearch?.call(searchModel);\n          }),\n        ),\n      ),\n    );\n  }\n\n  void searchDialog([TapDownDetails? details]) {\n    if (!searched) {\n      searchModel.searchOptions = {Option.url};\n    }\n    bool isEN = AppLocalizations.of(context)!.localeName == 'en';\n    var height = MediaQuery.of(context).size.height;\n    height = isEN ? height - 501 : height - 468;\n    if (Platform.isMacOS) {\n      height -= 30;\n    }\n    showMenu(\n        context: context,\n        position: RelativeRect.fromLTRB(65, height, 65, height),\n        constraints: const BoxConstraints(minWidth: 260, maxWidth: 330),\n        items: [\n          PopupMenuItem(\n              padding: const EdgeInsets.only(left: 15, right: 15, top: 10, bottom: 5),\n              enabled: false,\n              child: DefaultTextStyle.merge(\n                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 14),\n                  child: SizedBox(\n                      child: SearchConditions(\n                          searchModel: searchModel,\n                          onSearch: (val) {\n                            setState(() {\n                              searchModel = val;\n                              searched = searchModel.isNotEmpty;\n                              keywordController.text = searchModel.keyword ?? '';\n                              widget.onSearch?.call(searchModel);\n                            });\n                          }))))\n        ]);\n  }\n}\n\nclass ContentTypeSelect extends StatefulWidget {\n  final Function(ContentType? contentType) onSelected;\n\n  const ContentTypeSelect({super.key, required this.onSelected});\n\n  @override\n  State<StatefulWidget> createState() {\n    return ContentTypeState();\n  }\n}\n\nclass ContentTypeState extends State<ContentTypeSelect> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  String? value;\n  List<String>? types;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    value ??= localizations.all;\n    types ??= [\"JSON\", \"HTML\", \"JS\", \"CSS\", \"TEXT\", \"IMAGE\", localizations.all];\n\n    return PopupMenuButton(\n      initialValue: value,\n      offset: Offset(-10, (types!.length - types!.indexOf(value!)) * -30.0 - 10),\n      tooltip: localizations.responseType,\n      constraints: const BoxConstraints(maxWidth: 75),\n      child: Wrap(runAlignment: WrapAlignment.center, children: [\n        Text(value!, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),\n        const Icon(Icons.arrow_drop_up, size: 20)\n      ]),\n      onSelected: (String value) {\n        if (this.value == value) {\n          return;\n        }\n        setState(() {\n          this.value = value;\n        });\n        widget.onSelected(value == localizations.all ? null : ContentType.valueOf(value));\n      },\n      itemBuilder: (BuildContext context) {\n        return types!.map(item).toList();\n      },\n    );\n  }\n\n  PopupMenuItem<String> item(String value) {\n    return PopupMenuItem(\n      height: 30,\n      value: value,\n      child: Text(value, style: const TextStyle(fontSize: 12)),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/about.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/ui/app_update/app_update_repository.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass DesktopAbout extends StatefulWidget {\n  const DesktopAbout({super.key});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _AppUpdateStateChecking();\n  }\n}\n\nclass _AppUpdateStateChecking extends State<DesktopAbout> {\n  bool checkUpdating = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n    String gitHub = \"https://github.com/wanghongenpin/proxypin\";\n\n    return AlertDialog(\n      titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),\n      title: Row(mainAxisAlignment: MainAxisAlignment.center, children: [\n        const Expanded(child: SizedBox()),\n        Text(localizations.about, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),\n        const Expanded(child: SizedBox()),\n        const Align(alignment: Alignment.topRight, child: CloseButton())\n      ]),\n      content: SizedBox(\n          width: 360,\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              const Text(\"ProxyPin\", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, letterSpacing: .5)),\n              const SizedBox(height: 10),\n              Padding(\n                  padding: const EdgeInsets.only(left: 10, right: 10),\n                  child: Text(isCN ? \"全平台开源免费抓包软件\" : \"Full platform open source free capture HTTP(S) traffic software\",\n                      textAlign: TextAlign.center, style: const TextStyle(height: 1.3))),\n              const SizedBox(height: 10),\n              Padding(\n                padding: const EdgeInsets.symmetric(vertical: 6),\n                child: Text(\n                  \"Version ${AppConfiguration.version}\",\n                  style: TextStyle(fontWeight: FontWeight.w500),\n                ),\n              ),\n              const SizedBox(height: 12),\n              Divider(height: 1, color: Theme.of(context).dividerColor.withValues(alpha: 0.4)),\n              ListTile(\n                  dense: true,\n                  title: const Text('GitHub'),\n                  trailing: const Icon(Icons.open_in_new, size: 21),\n                  onTap: () => _safeLaunch(Uri.parse(gitHub))),\n              ListTile(\n                  dense: true,\n                  title: Text(localizations.feedback),\n                  trailing: const Icon(Icons.open_in_new, size: 21),\n                  onTap: () => _safeLaunch(Uri.parse(\"$gitHub/issues\"))),\n              ListTile(\n                  dense: true,\n                  title: Text(localizations.appUpdateCheckVersion),\n                  trailing: checkUpdating\n                      ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))\n                      : const Icon(Icons.sync, size: 21),\n                  onTap: () async {\n                    if (checkUpdating) return;\n                    setState(() => checkUpdating = true);\n                    await AppUpdateRepository.checkUpdate(context, canIgnore: false, showToast: true);\n                    if (mounted) setState(() => checkUpdating = false);\n                  }),\n              ListTile(\n                  dense: true,\n                  title: Text(isCN ? \"下载地址\" : \"Download\"),\n                  trailing: const Icon(Icons.open_in_new, size: 21),\n                  onTap: () => _safeLaunch(\n                      Uri.parse(isCN ? \"https://gitee.com/wanghongenpin/proxypin/releases\" : \"$gitHub/releases\"))),\n              ListTile(\n                  dense: true,\n                  title: Text(localizations.privacyPolicy),\n                  trailing: const Icon(Icons.privacy_tip_outlined, size: 21),\n                  onTap: () {\n                    showDialog(\n                        context: context,\n                        builder: (ctx) => AlertDialog(\n                              title: Text(localizations.privacyPolicy),\n                              content: SingleChildScrollView(\n                                  child: ConstrainedBox(\n                                      constraints: const BoxConstraints(maxWidth: 385),\n                                      child: Text(localizations.privacyContent, style: const TextStyle(height: 1.35)))),\n                              actions: [\n                                TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(localizations.close))\n                              ],\n                            ));\n                  }),\n              ListTile(\n                dense: true,\n                title: Text(localizations.sponsorDonate),\n                subtitle: Text(localizations.sponsorSupport, style: const TextStyle(fontSize: 11)),\n                trailing: const Icon(Icons.favorite, color: Colors.redAccent, size: 21),\n                onTap: () => _showSponsorDialog(),\n              ),\n            ],\n          )),\n    );\n  }\n\n  Future<void> _safeLaunch(Uri uri) async {\n    await launchUrl(uri, mode: LaunchMode.externalApplication);\n  }\n\n  void _showSponsorDialog() {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n    List<Widget> sponsors = [\n      ListTile(\n        onTap: () => _safeLaunch(Uri.parse(\"https://afdian.com/a/proxypin\")),\n        contentPadding: EdgeInsets.zero,\n        leading: const Icon(Icons.favorite, color: Colors.pinkAccent),\n        title: Text(localizations.sponsorAfdian),\n      )\n    ];\n\n    final coffee = ListTile(\n      contentPadding: EdgeInsets.zero,\n      leading: const Icon(Icons.coffee, color: Colors.brown),\n      title: Text('Buy Me a Coffee'),\n      onTap: () => _safeLaunch(Uri.parse(\"https://buymeacoffee.com/proxypin\")),\n    );\n    if (isCN) {\n      sponsors.add(coffee);\n    } else {\n      sponsors.insert(0, coffee);\n    }\n\n    showDialog(\n      context: context,\n      builder: (ctx) {\n        return AlertDialog(\n          title: Text(localizations.sponsorDonate),\n          contentPadding: const EdgeInsets.only(left: 20, top: 10, right: 20, bottom: 10),\n          content: SizedBox(\n            width: 340,\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(localizations.sponsorThanks, style: const TextStyle(height: 1.4)),\n                const SizedBox(height: 16),\n                ...sponsors\n              ],\n            ),\n          ),\n          actions: [\n            TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(localizations.close)),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/external_proxy.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\n/// @author wanghongen\n/// 2023/10/8\nclass ExternalProxyDialog extends StatefulWidget {\n  final Configuration configuration;\n\n  const ExternalProxyDialog({super.key, required this.configuration});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _ExternalProxyDialogState();\n  }\n}\n\nclass _ExternalProxyDialogState extends State<ExternalProxyDialog> {\n  final formKey = GlobalKey<FormState>();\n  late ProxyInfo externalProxy;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    externalProxy = ProxyInfo();\n    if (widget.configuration.externalProxy != null) {\n      externalProxy = ProxyInfo.fromJson(widget.configuration.externalProxy!.toJson());\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return AlertDialog(\n        scrollable: true,\n        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),\n        title: Text(localizations.externalProxy, style: const TextStyle(fontSize: 15)),\n        actions: [\n          TextButton(\n              onPressed: () {\n                Navigator.of(context).pop();\n              },\n              child: Text(localizations.cancel)),\n          TextButton(\n              onPressed: () async {\n                if (!formKey.currentState!.validate()) {\n                  return;\n                }\n                submit();\n              },\n              child: Text(localizations.confirm))\n        ],\n        content: Form(\n            key: formKey,\n            child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [\n              const SizedBox(height: 10),\n              Row(children: [\n                Expanded(child: Text(\"${localizations.enable}：\")),\n                Expanded(\n                    child: SwitchWidget(\n                  value: externalProxy.enabled,\n                  scale: 0.85,\n                  onChanged: (val) {\n                    externalProxy.enabled = val;\n                  },\n                ))\n              ]),\n\n              const SizedBox(height: 3),\n              Text(localizations.externalProxyServer, style: const TextStyle(fontWeight: FontWeight.w500)),\n              const SizedBox(height: 10),\n              SizedBox(\n                  height: 36,\n                  child: Row(children: [\n                    Expanded(\n                        child: TextFormField(\n                      initialValue: externalProxy.host,\n                      validator: (val) => val == null || val.isEmpty ? localizations.cannotBeEmpty : null,\n                      onChanged: (val) => externalProxy.host = val,\n                      decoration: const InputDecoration(\n                        contentPadding: EdgeInsets.symmetric(horizontal: 8),\n                        hintText: 'Host',\n                        hintStyle: TextStyle(color: Colors.grey),\n                        border: OutlineInputBorder(),\n                      ),\n                    )),\n                    const SizedBox(child: Text(' : ', style: TextStyle(fontSize: 22))),\n                    SizedBox(\n                        width: 65,\n                        child: TextFormField(\n                          initialValue: externalProxy.port?.toString() ?? '',\n                          inputFormatters: <TextInputFormatter>[\n                            LengthLimitingTextInputFormatter(5),\n                            FilteringTextInputFormatter.allow(RegExp(\"[0-9]\"))\n                          ],\n                          onChanged: (val) => externalProxy.port = int.parse(val),\n                          validator: (val) => val == null || val.isEmpty ? localizations.cannotBeEmpty : null,\n                          decoration: const InputDecoration(\n                            contentPadding: EdgeInsets.symmetric(horizontal: 8),\n                            hintText: 'Port',\n                            hintStyle: TextStyle(color: Colors.grey),\n                            border: OutlineInputBorder(),\n                          ),\n                        ))\n                  ])),\n\n              //认证\n              const SizedBox(height: 15),\n              Text(localizations.externalProxyAuth, style: const TextStyle(fontWeight: FontWeight.w500)),\n              const SizedBox(height: 10),\n              SizedBox(\n                  height: 36,\n                  child: Row(children: [\n                    SizedBox(\n                        width: isCN ? 65 : 85,\n                        child: Text('${localizations.username}：', style: const TextStyle(fontWeight: FontWeight.w300))),\n                    Expanded(\n                        child: TextFormField(\n                      initialValue: externalProxy.username,\n                      onChanged: (val) => externalProxy.username = val,\n                      decoration: const InputDecoration(\n                        contentPadding: EdgeInsets.symmetric(horizontal: 8),\n                        border: OutlineInputBorder(),\n                      ),\n                    ))\n                  ])),\n              const SizedBox(height: 10),\n\n              SizedBox(\n                  height: 36,\n                  child: Row(children: [\n                    SizedBox(\n                        width: isCN ? 65 : 85,\n                        child: Text('${localizations.password}：', style: const TextStyle(fontWeight: FontWeight.w300))),\n                    Expanded(\n                        child: TextFormField(\n                      initialValue: externalProxy.password,\n                      onChanged: (val) => externalProxy.password = val,\n                      decoration: const InputDecoration(\n                        contentPadding: EdgeInsets.symmetric(horizontal: 8),\n                        border: OutlineInputBorder(),\n                      ),\n                    ))\n                  ])),\n            ])));\n  }\n\n  submit() async {\n    bool setting = true;\n    if (externalProxy.enabled) {\n      try {\n        var socket = await Socket.connect(externalProxy.host, externalProxy.port!, timeout: const Duration(seconds: 1));\n        socket.destroy();\n      } on SocketException catch (_) {\n        setting = false;\n        if (mounted) {\n          await showDialog(\n              context: context,\n              builder: (_) => AlertDialog(\n                    title: Text(localizations.externalProxyConnectFailure),\n                    content: SizedBox(\n                        width: 230,\n                        child: Text(localizations.externalProxyFailureConfirm,\n                            style: const TextStyle(fontSize: 12), maxLines: 3)),\n                    actions: [\n                      TextButton(\n                          onPressed: () {\n                            Navigator.of(context).pop();\n                          },\n                          child: Text(localizations.cancel)),\n                      TextButton(\n                          onPressed: () {\n                            setting = true;\n                            Navigator.of(context).pop();\n                          },\n                          child: Text(localizations.confirm))\n                    ],\n                  ));\n        }\n      }\n    }\n\n    if (setting) {\n      widget.configuration.externalProxy = externalProxy;\n      widget.configuration.flushConfig();\n    }\n\n    if (!mounted) return;\n    Navigator.of(context).pop();\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/filter.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/components/host_filter.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\n/// @author wanghongen\n/// 2023/10/8\nclass FilterDialog extends StatefulWidget {\n  final Configuration configuration;\n\n  const FilterDialog({super.key, required this.configuration});\n\n  @override\n  State<FilterDialog> createState() => _FilterDialogState();\n}\n\nclass _FilterDialogState extends State<FilterDialog> {\n  final ValueNotifier<bool> hostEnableNotifier = ValueNotifier(false);\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void dispose() {\n    hostEnableNotifier.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),\n        titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),\n        contentPadding: const EdgeInsets.only(left: 20, right: 20),\n        scrollable: true,\n        title: Row(children: [\n          const Expanded(child: SizedBox()),\n          Text(localizations.domainFilter, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),\n          const Expanded(child: SizedBox()),\n          Align(alignment: Alignment.topRight, child: CloseButton())\n        ]),\n        content: SizedBox(\n          width: 680,\n          height: 510,\n          child: Flex(\n            direction: Axis.horizontal,\n            children: [\n              Expanded(\n                  flex: 1,\n                  child: DomainFilter(\n                      title: localizations.domainWhitelist,\n                      subtitle: localizations.domainWhitelistDescribe,\n                      hostList: HostFilter.whitelist,\n                      configuration: widget.configuration,\n                      hostEnableNotifier: hostEnableNotifier)),\n              const SizedBox(width: 10),\n              Expanded(\n                  flex: 1,\n                  child: DomainFilter(\n                      title: localizations.domainBlacklist,\n                      subtitle: localizations.domainBlacklistDescribe,\n                      hostList: HostFilter.blacklist,\n                      configuration: widget.configuration,\n                      hostEnableNotifier: hostEnableNotifier)),\n            ],\n          ),\n        ));\n  }\n}\n\nclass DomainFilter extends StatefulWidget {\n  final String title;\n  final String subtitle;\n  final HostList hostList;\n  final Configuration configuration;\n  final ValueNotifier<bool> hostEnableNotifier;\n\n  const DomainFilter(\n      {super.key,\n      required this.title,\n      required this.subtitle,\n      required this.hostList,\n      required this.hostEnableNotifier,\n      required this.configuration});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _DomainFilterState();\n  }\n}\n\nclass _DomainFilterState extends State<DomainFilter> {\n  bool changed = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void dispose() {\n    if (changed) {\n      widget.configuration.flushConfig();\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        ListTile(\n          title: Text(widget.title),\n          isThreeLine: true,\n          subtitle: Text(widget.subtitle, style: const TextStyle(fontSize: 12)),\n          titleAlignment: ListTileTitleAlignment.center,\n        ),\n        Row(children: [\n          const SizedBox(width: 8),\n          Text(localizations.enable),\n          const SizedBox(width: 10),\n          SwitchWidget(\n              scale: 0.75,\n              value: widget.hostList.enabled,\n              onChanged: (value) {\n                widget.hostList.enabled = value;\n                changed = true;\n              }),\n          const Expanded(child: SizedBox()),\n          TextButton.icon(icon: const Icon(Icons.add, size: 18), onPressed: add, label: Text(localizations.add)),\n          const SizedBox(width: 5),\n          TextButton.icon(\n              icon: const Icon(Icons.input_rounded, size: 18), onPressed: import, label: Text(localizations.import)),\n          const SizedBox(width: 5),\n        ]),\n        DomainList(widget.hostList, onChange: () => changed = true)\n      ],\n    );\n  }\n\n  //导入\n  import() async {\n\n    final FilePickerResult? result =\n        await FilePicker.platform.pickFiles(allowedExtensions: ['config'], type: FileType.custom, initialDirectory: \"/Downloads\");\n    var file = result?.files.single;\n    if (file == null) {\n      return;\n    }\n\n    try {\n      List json = jsonDecode(await file.xFile.readAsString());\n      for (var item in json) {\n        widget.hostList.add(item);\n      }\n\n      changed = true;\n      if (mounted) {\n        FlutterToastr.show(localizations.importSuccess, context);\n      }\n      setState(() {});\n    } catch (e, t) {\n      logger.e('导入失败 $file', error: e, stackTrace: t);\n      if (mounted) {\n        FlutterToastr.show(\"${localizations.importFailed} $e\", context);\n      }\n    }\n  }\n\n  void add() {\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (BuildContext context) => DomainAddDialog(hostList: widget.hostList)).then((value) {\n      if (value != null) {\n        setState(() {\n          changed = true;\n        });\n      }\n    });\n  }\n}\n\nclass DomainAddDialog extends StatelessWidget {\n  final HostList hostList;\n  final int? index;\n\n  const DomainAddDialog({super.key, required this.hostList, this.index});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    GlobalKey formKey = GlobalKey<FormState>();\n    String? host = index == null ? null : hostList.list.elementAt(index!).pattern.replaceAll(\".*\", \"*\");\n    return AlertDialog(\n        scrollable: true,\n        content: Padding(\n            padding: const EdgeInsets.all(8.0),\n            child: Form(\n                key: formKey,\n                child: Column(children: <Widget>[\n                  TextFormField(\n                      initialValue: host,\n                      decoration: const InputDecoration(labelText: 'Host', hintText: '*.example.com'),\n                      validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,\n                      onChanged: (val) => host = val)\n                ]))),\n        actions: [\n          TextButton(child: Text(localizations.cancel), onPressed: () => Navigator.of(context).pop()),\n          TextButton(\n              child: Text(localizations.save),\n              onPressed: () {\n                if (!(formKey.currentState as FormState).validate()) {\n                  return;\n                }\n                try {\n                  if (index != null) {\n                    hostList.list[index!] = RegExp(host!.trim().replaceAll(\"*\", \".*\"));\n                  } else {\n                    hostList.add(host!.trim());\n                  }\n                } catch (e) {\n                  ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));\n                }\n                Navigator.of(context).pop(host);\n              }),\n        ]);\n  }\n}\n\n///域名列表\nclass DomainList extends StatefulWidget {\n  final HostList hostList;\n  final Function onChange;\n\n  const DomainList(this.hostList, {super.key, required this.onChange});\n\n  @override\n  State<StatefulWidget> createState() => _DomainListState();\n}\n\nclass _DomainListState extends State<DomainList> {\n  Map<int, bool> selected = {};\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n  bool isPressed = false;\n  Offset? lastPressPosition;\n  bool changed = false;\n  bool _isSecondaryTapHandled = false;\n\n  onChanged() {\n    changed = true;\n    widget.onChange.call();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n        onSecondaryTapDown: (details) => showGlobalMenu(details.globalPosition),\n        onTapDown: (details) {\n          if (selected.isEmpty) {\n            return;\n          }\n\n          if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n            return;\n          }\n          setState(() {\n            selected.clear();\n          });\n        },\n        child: Listener(\n            onPointerUp: (event) => isPressed = false,\n            onPointerDown: (event) {\n              lastPressPosition = event.localPosition;\n              if (event.buttons == kPrimaryMouseButton) {\n                isPressed = true;\n              }\n            },\n            child: Container(\n                padding: const EdgeInsets.only(top: 10),\n                height: 380,\n                decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n                child: SingleChildScrollView(\n                    child: Column(children: [\n                  Row(\n                    mainAxisAlignment: MainAxisAlignment.start,\n                    children: [\n                      Container(width: 15),\n                      const Expanded(child: Text('Host')),\n                    ],\n                  ),\n                  const Divider(thickness: 0.5),\n                  Column(children: rows(widget.hostList.list))\n                ])))));\n  }\n\n  List<Widget> rows(List<RegExp> list) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n\n    return List.generate(list.length, (index) {\n      return InkWell(\n          highlightColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          hoverColor: primaryColor.withOpacity(0.3),\n          onSecondaryTapDown: (details) => showMenus(details, index),\n          //right click menus\n          onDoubleTap: () => showEdit(index),\n          onHover: (hover) {\n            if (isPressed && selected[index] != true) {\n              setState(() {\n                selected[index] = true;\n              });\n            }\n          },\n          onTap: () {\n            if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n              setState(() {\n                selected[index] = !(selected[index] ?? false);\n              });\n              return;\n            }\n            if (selected.isEmpty) {\n              return;\n            }\n            setState(() {\n              selected.clear();\n            });\n          },\n          child: Container(\n              color: selected[index] == true\n                  ? primaryColor.withOpacity(0.6)\n                  : index.isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 38,\n              padding: const EdgeInsets.symmetric(vertical: 3),\n              child: Row(\n                children: [\n                  const SizedBox(width: 15),\n                  Expanded(\n                      child: Text(list[index].pattern.replaceAll(\".*\", \"*\"), style: const TextStyle(fontSize: 14))),\n                ],\n              )));\n    });\n  }\n\n  //导出\n  export(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n\n    String fileName = 'host-filters.config';\n    String? saveLocation = (await FilePicker.platform.saveFile(fileName: fileName));\n    if (saveLocation == null) {\n      return;\n    }\n\n    var list = [];\n    for (var index in indexes) {\n      String rule = widget.hostList.list[index].pattern.replaceAll(\".*\", \"*\");\n      list.add(rule);\n    }\n\n    await File(saveLocation).writeAsBytes(utf8.encode(jsonEncode(list)));\n\n    if (mounted) {\n      FlutterToastr.show(localizations.exportSuccess, context);\n    }\n  }\n\n  //删除\n  Future<void> remove(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    return showConfirmDialog(context, content: localizations.requestRewriteDeleteConfirm(indexes.length),\n        onConfirm: () async {\n      widget.hostList.removeIndex(indexes);\n      onChanged();\n      setState(() {\n        selected.clear();\n      });\n      if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n    });\n  }\n\n  showEdit([int? index]) {\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (BuildContext context) {\n          return DomainAddDialog(hostList: widget.hostList, index: index);\n        }).then((value) {\n      if (value != null) {\n        setState(() {\n          onChanged();\n        });\n      }\n    });\n  }\n\n  showGlobalMenu(Offset offset) {\n    if (_isSecondaryTapHandled) {\n      return;\n    }\n\n    showContextMenu(context, offset, items: [\n      PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()),\n      PopupMenuItem(\n          height: 35,\n          enabled: selected.isNotEmpty,\n          child: Text(localizations.export),\n          onTap: () => export(selected.keys.toList())),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35,\n          enabled: selected.isNotEmpty,\n          child: Text(localizations.deleteSelect),\n          onTap: () => remove(selected.keys.toList())),\n    ]);\n  }\n\n  //点击菜单\n  showMenus(TapDownDetails details, int index) {\n    if (selected.isNotEmpty) {\n      showGlobalMenu(details.globalPosition);\n      return;\n    }\n\n    _isSecondaryTapHandled = true;\n    setState(() {\n      selected[index] = true;\n    });\n\n    showContextMenu(context, details.globalPosition, items: [\n      PopupMenuItem(\n          height: 35,\n          child: Text(localizations.copy),\n          onTap: () {\n            Clipboard.setData(ClipboardData(text: widget.hostList.list[index].pattern.replaceAll(\".*\", \"*\")));\n            FlutterToastr.show(localizations.copied, context);\n          }),\n      PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)),\n      PopupMenuItem(height: 35, onTap: () => export([index]), child: Text(localizations.export)),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35,\n          child: Text(localizations.delete),\n          onTap: () {\n            widget.hostList.removeIndex([index]);\n            onChanged();\n          })\n    ]).then((value) {\n      _isSecondaryTapHandled = false;\n      setState(() {\n        selected.remove(index);\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/hosts.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/components/manager/hosts_manager.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\n///hosts设置\n///@author Hongen Wang\nclass HostsDialog extends StatefulWidget {\n  final HostsManager hostsManager;\n\n  const HostsDialog({super.key, required this.hostsManager});\n\n  @override\n  State<HostsDialog> createState() => _HostsDialogState();\n}\n\nclass _HostsDialogState extends State<HostsDialog> {\n  Set<HostsItem> selected = {};\n  Set<String> offstage = {};\n\n  bool isPressed = false;\n  Offset? lastPressPosition;\n\n  bool saving = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  saveConfig() {\n    if (saving) return;\n    saving = true;\n    Future.delayed(const Duration(milliseconds: 3000), () {\n      widget.hostsManager.flushConfig();\n      saving = false;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n        onSecondaryTap: () {\n          if (lastPressPosition == null) {\n            return;\n          }\n          showGlobalMenu(lastPressPosition!);\n        },\n        onTapDown: (details) {\n          if (selected.isEmpty) {\n            return;\n          }\n\n          if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n            return;\n          }\n          setState(() {\n            selected.clear();\n          });\n        },\n        child: Listener(\n            onPointerUp: (event) => isPressed = false,\n            onPointerDown: (event) {\n              lastPressPosition = event.localPosition;\n              if (event.buttons == kPrimaryMouseButton) {\n                isPressed = true;\n              }\n            },\n            child: AlertDialog(\n                titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),\n                contentPadding: const EdgeInsets.symmetric(horizontal: 15),\n                scrollable: true,\n                title: Row(mainAxisAlignment: MainAxisAlignment.center, children: [\n                  const Expanded(child: SizedBox()),\n                  Text('Hosts', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),\n                  const Expanded(child: SizedBox()),\n                  Align(alignment: Alignment.topRight, child: CloseButton())\n                ]),\n                content: SizedBox(\n                  width: 550,\n                  height: 500,\n                  child: Column(children: [\n                    Row(children: [\n                      Container(width: 15),\n                      Text(localizations.enable),\n                      const SizedBox(width: 10),\n                      SwitchWidget(\n                          scale: 0.8,\n                          value: widget.hostsManager.enabled,\n                          onChanged: (value) {\n                            widget.hostsManager.enabled = value;\n                            saveConfig();\n                          }),\n                      const Expanded(child: SizedBox()),\n                      TextButton.icon(\n                          icon: const Icon(Icons.add, size: 18),\n                          onPressed: showEdit,\n                          label: Text(localizations.newBuilt)),\n                      const SizedBox(width: 5),\n                      TextButton.icon(\n                          icon: const Icon(Icons.folder_outlined, size: 18),\n                          onPressed: newFolder,\n                          label: Text(localizations.newFolder)),\n                      const SizedBox(width: 5),\n                      TextButton.icon(\n                          icon: const Icon(Icons.input_rounded, size: 18),\n                          onPressed: import,\n                          label: Text(localizations.import)),\n                      const SizedBox(width: 5),\n                    ]),\n                    const SizedBox(height: 8),\n                    Container(\n                        height: 430,\n                        decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n                        child: Column(children: [\n                          const SizedBox(height: 5),\n                          Row(children: [\n                            Container(width: 15),\n                            SizedBox(\n                                width: 50, child: Text(localizations.enable, style: const TextStyle(fontSize: 14))),\n                            Container(width: 15),\n                            Expanded(child: Text(localizations.domain, style: TextStyle(fontSize: 14))),\n                            Container(width: 15),\n                            Expanded(child: Text(localizations.toAddress, style: const TextStyle(fontSize: 14))),\n                          ]),\n                          const Divider(thickness: 0.5),\n                          Expanded(\n                              child: ListView.builder(\n                                  shrinkWrap: true,\n                                  itemCount: widget.hostsManager.list.length,\n                                  padding: const EdgeInsets.only(right: 10),\n                                  itemBuilder: (_, index) => row(widget.hostsManager.list[index], index.isEven)))\n                        ])),\n                  ]),\n                ))));\n  }\n\n  Widget row(HostsItem item, bool isEven, {EdgeInsetsGeometry? padding}) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n\n    return Column(children: [\n      InkWell(\n          highlightColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          hoverColor: primaryColor.withOpacity(0.3),\n          onSecondaryTapDown: (details) => showMenus(details, item),\n          onDoubleTap: item.isFolder ? null : () => showEdit(item: item),\n          onTap: () {\n            if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n              setState(() {\n                selected.contains(item) ? selected.remove(item) : selected.add(item);\n              });\n              return;\n            }\n\n            if (!isPressed && selected.isNotEmpty) {\n              setState(() {\n                selected.clear();\n              });\n              return;\n            }\n\n            if (item.isFolder) {\n              setState(() {\n                offstage.contains(item.id) ? offstage.remove(item.id) : offstage.add(item.id);\n              });\n            }\n          },\n          onHover: (hover) {\n            if (isPressed && !selected.contains(item)) {\n              setState(() {\n                selected.add(item);\n              });\n            }\n          },\n          child: Container(\n              color: selected.contains(item)\n                  ? primaryColor.withOpacity(0.6)\n                  : isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 35,\n              padding: padding ?? const EdgeInsets.symmetric(vertical: 3),\n              child: Row(\n                children: [\n                  SwitchWidget(\n                      scale: 0.6,\n                      value: item.enabled,\n                      onChanged: (val) {\n                        setState(() {\n                          item.enabled = val;\n                          saveConfig();\n                        });\n                      }),\n                  Container(width: 15),\n                  Expanded(\n                      child: IconText(\n                          icon: item.isFolder\n                              ? Icon(offstage.contains(item.id) ? Icons.folder : Icons.folder_outlined, size: 18)\n                              : null,\n                          text: item.host,\n                          textStyle: const TextStyle(fontSize: 14))),\n                  Container(width: 15),\n                  Expanded(child: Text(item.toAddress ?? '', style: const TextStyle(fontSize: 14)))\n                ],\n              ))),\n      if (item.isFolder)\n        Offstage(\n            offstage: offstage.contains(item.id),\n            child: Column(\n                children: widget.hostsManager\n                    .getFolderList(item.id)\n                    .map((e) => row(e, !isEven, padding: EdgeInsets.only(left: 60)))\n                    .toList()))\n    ]);\n  }\n\n  newFolder() {\n    showEdit(isFolder: true);\n  }\n\n  enableStatus(bool enable) {\n    if (selected.isEmpty) return;\n\n    for (var item in selected) {\n      if (item.enabled == enable) continue;\n      item.enabled = enable;\n    }\n    setState(() {\n      saveConfig();\n    });\n  }\n\n  showGlobalMenu(Offset offset) {\n    showContextMenu(context, offset, items: [\n      PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()),\n      PopupMenuItem(\n          height: 35, enabled: selected.isNotEmpty, child: Text(localizations.export), onTap: () => export(selected)),\n      const PopupMenuDivider(),\n      PopupMenuItem(height: 35, child: Text(localizations.enableSelect), onTap: () => enableStatus(true)),\n      PopupMenuItem(height: 35, child: Text(localizations.disableSelect), onTap: () => enableStatus(false)),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35,\n          enabled: selected.isNotEmpty,\n          child: Text(localizations.deleteSelect),\n          onTap: () => removeHosts(selected)),\n    ]);\n  }\n\n  //点击菜单\n  showMenus(TapDownDetails details, HostsItem item) {\n    if (selected.length > 1) {\n      showGlobalMenu(details.globalPosition);\n      return;\n    }\n\n    setState(() {\n      selected.add(item);\n    });\n\n    showContextMenu(context, details.globalPosition, items: [\n      if (item.isFolder)\n        PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit(parent: item)),\n      PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(item: item)),\n      PopupMenuItem(height: 35, onTap: () => export([item]), child: Text(localizations.export)),\n      PopupMenuItem(\n          height: 35,\n          child: item.enabled ? Text(localizations.disabled) : Text(localizations.enable),\n          onTap: () {\n            setState(() {\n              item.enabled = !item.enabled;\n              saveConfig();\n            });\n          }),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35,\n          child: Text(localizations.delete),\n          onTap: () async {\n            setState(() {\n              widget.hostsManager.removeHosts([item]);\n            });\n          })\n    ]).then((value) {\n      setState(() {\n        selected.remove(item);\n      });\n    });\n  }\n\n  showEdit({HostsItem? item, HostsItem? parent, bool? isFolder}) {\n    isFolder ??= item?.isFolder == true;\n    showDialog(\n        context: context,\n        builder: (BuildContext context) => isFolder == true\n            ? FolderDialog(hostsManager: widget.hostsManager, folder: item)\n            : HostsEditDialog(item: item, parent: parent)).then((value) {\n      if (value != null) {\n        setState(() {\n          saveConfig();\n        });\n      }\n    });\n  }\n\n  //删除\n  Future<void> removeHosts(Set<HostsItem> items) async {\n    if (items.isEmpty) return;\n    return showConfirmDialog(context, onConfirm: () async {\n      await widget.hostsManager.removeHosts(items);\n      setState(() {\n        items.clear();\n      });\n      if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n    });\n  }\n\n  //导入\n  import() async {\n    final FilePickerResult? result = await FilePicker.platform\n        .pickFiles(allowedExtensions: ['json'], type: FileType.custom, initialDirectory: \"/Downloads\");\n    var file = result?.files.single;\n    if (file == null) {\n      return;\n    }\n\n    try {\n      List json = jsonDecode(await file.xFile.readAsString());\n      Map<String, String> idMap = {};\n\n      for (var item in json) {\n        //生成新的id 保存映射关系\n        String newId = HostsItem.generateId();\n        idMap[item['id']] = newId;\n        item['id'] = newId;\n        var hostsItem = HostsItem.fromJson(item);\n\n        if (hostsItem.parent != null) {\n          hostsItem.parent = idMap[hostsItem.parent!];\n        }\n\n        widget.hostsManager.addHosts(hostsItem);\n      }\n\n      saveConfig();\n      if (mounted) {\n        FlutterToastr.show(localizations.importSuccess, context);\n      }\n      setState(() {});\n    } catch (e, t) {\n      logger.e('导入失败 $file', error: e, stackTrace: t);\n      if (mounted) {\n        FlutterToastr.show(\"${localizations.importFailed} $e\", context);\n      }\n    }\n  }\n\n  //导出\n  export(Iterable<HostsItem> items) async {\n    if (items.isEmpty) return;\n\n    String fileName = 'hosts.json';\n    var path = await FilePicker.platform.saveFile(fileName: fileName);\n    if (path == null) {\n      return;\n    }\n\n    var list = [];\n    for (var item in items) {\n      var json = item.toJson();\n      list.add(json);\n    }\n\n    await File(path).writeAsBytes(utf8.encode(jsonEncode(list)));\n    if (mounted) FlutterToastr.show(localizations.exportSuccess, context);\n  }\n}\n\nclass FolderDialog extends StatelessWidget {\n  final HostsManager hostsManager;\n  final HostsItem? folder;\n\n  const FolderDialog({super.key, required this.hostsManager, this.folder});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    bool enabled = folder?.enabled ?? true;\n    String name = folder?.host ?? '';\n\n    return AlertDialog(\n      title: Text(localizations.newFolder, style: const TextStyle(fontSize: 16)),\n      content: Column(mainAxisSize: MainAxisSize.min, children: [\n        Row(children: [\n          SizedBox(width: 55, child: Text(localizations.enable)),\n          SwitchWidget(scale: 0.8, value: enabled, onChanged: (value) => enabled = value)\n        ]),\n        SizedBox(height: 10),\n        Row(children: [\n          SizedBox(width: 55, child: Text(localizations.name)),\n          Expanded(\n              child: TextFormField(\n                  initialValue: name,\n                  onChanged: (val) => name = val,\n                  decoration: InputDecoration(border: OutlineInputBorder())))\n        ])\n      ]),\n      actions: [\n        TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n        TextButton(\n            onPressed: () {\n              HostsItem item;\n              if (folder == null) {\n                item = HostsItem(isFolder: true, host: name, enabled: enabled);\n                hostsManager.addHosts(item);\n              } else {\n                folder!.enabled = enabled;\n                folder!.host = name;\n                item = folder!;\n              }\n              Navigator.pop(context, item);\n            },\n            child: Text(localizations.save)),\n      ],\n    );\n  }\n}\n\nclass HostsEditDialog extends StatefulWidget {\n  final HostsItem? item;\n  final HostsItem? parent;\n\n  const HostsEditDialog({super.key, this.item, this.parent});\n\n  @override\n  State<HostsEditDialog> createState() => _HostsEditDialogState();\n}\n\nclass _HostsEditDialogState extends State<HostsEditDialog> {\n  GlobalKey formKey = GlobalKey<FormState>();\n\n  bool enabled = true;\n  TextEditingController hostController = TextEditingController();\n  TextEditingController toAddressController = TextEditingController();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.item != null) {\n      enabled = widget.item!.enabled;\n      hostController.text = widget.item!.host;\n      toAddressController.text = widget.item!.toAddress ?? '';\n    }\n  }\n\n  @override\n  void dispose() {\n    hostController.dispose();\n    toAddressController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n        contentPadding: const EdgeInsets.only(left: 20, right: 20, top: 10),\n        actions: [\n          TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n          TextButton(\n              onPressed: () {\n                if (!(formKey.currentState as FormState).validate()) {\n                  FlutterToastr.show(\n                      \"${localizations.domain} ${localizations.toAddress} ${localizations.cannotBeEmpty}\", context,\n                      position: FlutterToastr.center);\n                  return;\n                }\n\n                HostsItem? hostItem;\n                if (widget.item == null) {\n                  hostItem = HostsItem(\n                      enabled: enabled,\n                      parent: widget.parent?.id,\n                      host: hostController.text,\n                      toAddress: toAddressController.text);\n                  HostsManager.instance.then((it) => it.addHosts(hostItem!));\n                } else {\n                  widget.item!.enabled = enabled;\n                  widget.item!.host = hostController.text;\n                  widget.item!.toAddress = toAddressController.text;\n                  hostItem = widget.item;\n                }\n\n                Navigator.pop(context, hostItem);\n              },\n              child: Text(localizations.save)),\n        ],\n        content: SizedBox(\n          width: 300,\n          height: 180,\n          child: Form(\n              key: formKey,\n              child: Column(children: [\n                Row(children: [\n                  SizedBox(width: 80, child: Text(localizations.enable)),\n                  Expanded(child: SwitchWidget(scale: 0.8, value: enabled, onChanged: (value) => enabled = value)),\n                ]),\n                const SizedBox(height: 8),\n                Row(children: [\n                  SizedBox(width: 80, child: Text(localizations.domain)),\n                  Expanded(\n                      child: TextFormField(\n                          controller: hostController,\n                          validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,\n                          decoration: const InputDecoration(\n                              isDense: true,\n                              hintText: '*.example.com',\n                              hintStyle: TextStyle(color: Colors.grey),\n                              errorStyle: TextStyle(height: 0, fontSize: 0),\n                              border: OutlineInputBorder()))),\n                ]),\n                const SizedBox(height: 15),\n                Row(children: [\n                  SizedBox(width: 80, child: Text(localizations.toAddress)),\n                  Expanded(\n                      child: TextFormField(\n                          controller: toAddressController,\n                          validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,\n                          decoration: const InputDecoration(\n                              isDense: true,\n                              hintText: '202.108.22.5',\n                              errorStyle: TextStyle(height: 0, fontSize: 0),\n                              hintStyle: TextStyle(color: Colors.grey),\n                              border: OutlineInputBorder()))),\n                ]),\n              ])),\n        ));\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/request_block.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_block_manager.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\nclass RequestBlock extends StatefulWidget {\n  final RequestBlockManager requestBlockManager;\n\n  const RequestBlock({super.key, required this.requestBlockManager});\n\n  @override\n  State<RequestBlock> createState() => _RequestBlockState();\n}\n\nclass _RequestBlockState extends State<RequestBlock> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n  bool changed = false;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    if (changed) {\n      widget.requestBlockManager.flushConfig();\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n        titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),\n        contentPadding: const EdgeInsets.only(left: 20, right: 20),\n        scrollable: true,\n        title: Row(children: [\n          const Expanded(child: SizedBox()),\n          Text(localizations.requestBlock, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n          const Expanded(child: SizedBox()),\n          Align(alignment: Alignment.topRight, child: CloseButton())\n        ]),\n        content: SizedBox(\n            width: 550,\n            height: 500,\n            child: Column(children: [\n              Row(children: [\n                const SizedBox(width: 8),\n                Text(localizations.enable),\n                const SizedBox(width: 10),\n                SwitchWidget(\n                    scale: 0.8,\n                    value: widget.requestBlockManager.enabled,\n                    onChanged: (value) {\n                      widget.requestBlockManager.enabled = value;\n                      changed = true;\n                    }),\n                const Expanded(child: SizedBox()),\n                TextButton.icon(\n                    icon: const Icon(Icons.add, size: 18), onPressed: showEdit, label: Text(localizations.add)),\n                const SizedBox(width: 5),\n              ]),\n              const SizedBox(height: 8),\n              Container(\n                  height: 430,\n                  decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n                  child: Column(children: [\n                    const SizedBox(height: 5),\n                    Row(children: [\n                      Container(width: 15),\n                      const Expanded(child: Text('URL', style: TextStyle(fontSize: 14))),\n                      SizedBox(width: 80, child: Text(localizations.enable, style: const TextStyle(fontSize: 14))),\n                      Container(width: 18),\n                      SizedBox(width: 120, child: Text(localizations.action, style: const TextStyle(fontSize: 14))),\n                    ]),\n                    const Divider(thickness: 0.5),\n                    Expanded(\n                        child: ListView.builder(\n                            itemCount: widget.requestBlockManager.list.length, itemBuilder: (_, index) => row(index)))\n                  ]))\n            ])));\n  }\n\n  Widget row(int index) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n    bool isCN = localizations.localeName == 'zh';\n    var list = widget.requestBlockManager.list;\n\n    return InkWell(\n        highlightColor: Colors.transparent,\n        splashColor: Colors.transparent,\n        hoverColor: primaryColor.withOpacity(0.3),\n        onSecondaryTapDown: (details) => showMenus(details, index),\n        onDoubleTap: () => showEdit(index),\n        child: Container(\n            color: index.isEven ? Colors.grey.withOpacity(0.10) : null,\n            height: 36,\n            padding: const EdgeInsets.symmetric(vertical: 3),\n            child: Row(\n              children: [\n                const SizedBox(width: 10),\n                Expanded(child: Text(list[index].url, style: const TextStyle(fontSize: 14))),\n                const SizedBox(width: 20),\n                SwitchWidget(\n                    scale: 0.65,\n                    value: list[index].enabled,\n                    onChanged: (val) {\n                      list[index].enabled = val;\n                      setState(() {\n                        changed = true;\n                      });\n                    }),\n                const SizedBox(width: 40),\n                SizedBox(\n                    width: 130,\n                    child: Text(isCN ? list[index].type.label : list[index].type.name,\n                        style: const TextStyle(fontSize: 14)))\n              ],\n            )));\n  }\n\n  //点击菜单\n  showMenus(TapDownDetails details, int index) {\n    var list = widget.requestBlockManager.list;\n\n    showContextMenu(context, details.globalPosition, items: [\n      PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)),\n      PopupMenuItem(\n          height: 35,\n          child: list[index].enabled ? Text(localizations.disabled) : Text(localizations.enable),\n          onTap: () {\n            list[index].enabled = !list[index].enabled;\n            changed = true;\n            setState(() {});\n          }),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35,\n          child: Text(localizations.delete),\n          onTap: () async {\n            await widget.requestBlockManager.removeBlockRequest(index);\n            setState(() {});\n          })\n    ]);\n  }\n\n  showEdit([int? index]) {\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (BuildContext context) {\n          return RequestBlockAddDialog(requestBlockManager: widget.requestBlockManager, index: index);\n        }).then((value) {\n      if (value != null) {\n        setState(() {\n          changed = true;\n        });\n      }\n    });\n  }\n}\n\nclass RequestBlockAddDialog extends StatelessWidget {\n  final RequestBlockManager requestBlockManager;\n  final int? index;\n\n  const RequestBlockAddDialog({super.key, required this.requestBlockManager, this.index});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    bool isCN = localizations.localeName == 'zh';\n\n    GlobalKey formKey = GlobalKey<FormState>();\n    RequestBlockItem item =\n        index == null ? RequestBlockItem(true, '', BlockType.values.first) : requestBlockManager.list.elementAt(index!);\n    bool enabled = item.enabled;\n    return AlertDialog(\n        scrollable: true,\n        content: Padding(\n            padding: const EdgeInsets.all(8.0),\n            child: Form(\n                key: formKey,\n                child: Column(children: <Widget>[\n                  SwitchWidget(title: localizations.enable, value: item.enabled, onChanged: (val) => enabled = val),\n                  const SizedBox(height: 20),\n                  TextFormField(\n                      initialValue: item.url,\n                      decoration: const InputDecoration(\n                          isDense: true,\n                          labelText: 'URL',\n                          hintText: 'https://example.com/*',\n                          border: OutlineInputBorder()),\n                      validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,\n                      onSaved: (val) => item.url = val!.trim()),\n                  const SizedBox(height: 20),\n                  DropdownButtonFormField(\n                      value: item.type,\n                      decoration: InputDecoration(\n                          isDense: true, labelText: localizations.type, border: const OutlineInputBorder()),\n                      items: BlockType.values\n                          .map((e) => DropdownMenuItem(\n                              value: e, child: Text(isCN ? e.label : e.name, style: const TextStyle(fontSize: 14))))\n                          .toList(),\n                      onSaved: (val) => item.type = val!,\n                      onChanged: (val) {}),\n                ]))),\n        actions: [\n          TextButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop()),\n          TextButton(\n              child: Text(localizations.save),\n              onPressed: () {\n                if (!(formKey.currentState as FormState).validate()) {\n                  return;\n                }\n                (formKey.currentState as FormState).save();\n\n                item.enabled = enabled;\n                item.urlReg = null;\n                if (index != null) {\n                  requestBlockManager.list[index!] = item;\n                } else {\n                  requestBlockManager.addBlockRequest(item);\n                }\n                Navigator.of(context).pop(item);\n              }),\n        ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/request_breakpoint.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_breakpoint_manager.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\nimport '../../component/app_dialog.dart' show CustomToast;\nimport '../../component/http_method_popup.dart';\n\nclass RequestBreakpointPage extends StatefulWidget {\n  final RequestBreakpointManager manager;\n  final int? windowId;\n\n  const RequestBreakpointPage({super.key, this.windowId, required this.manager});\n\n  @override\n  State<RequestBreakpointPage> createState() => _RequestBreakpointPageState();\n}\n\nclass _RequestBreakpointPageState extends State<RequestBreakpointPage> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n  List<RequestBreakpointRule> rules = [];\n  bool enabled = false;\n\n  RequestBreakpointManager get manager => widget.manager;\n\n  Set<int> selected = {};\n  bool isPressed = false;\n  Offset? lastPressPosition;\n\n  Future<void> _refreshConfig() async {\n    if (widget.windowId != null) {\n      await DesktopMultiWindow.invokeMethod(0, \"refreshRequestBreakpoint\");\n    }\n  }\n\n  Future<void> _save() async {\n    await manager.save();\n    await _refreshConfig();\n  }\n\n  Future<void> _import() async {\n    String? path;\n    if (Platform.isMacOS) {\n      path = await DesktopMultiWindow.invokeMethod(0, \"pickFiles\", {\n        \"allowedExtensions\": ['json']\n      });\n      if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show();\n    } else {\n      FilePickerResult? result =\n          await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']);\n      path = result?.files.single.path;\n    }\n    if (path == null) return;\n    File file = File(path);\n    try {\n      String content = await file.readAsString();\n      List<dynamic> list = jsonDecode(content);\n      var rules = list.map((e) => RequestBreakpointRule.fromJson(e)).toList();\n      for (var rule in rules) {\n        manager.list.add(rule);\n      }\n      await _save();\n      setState(() {\n        this.rules = manager.list;\n      });\n\n      if (mounted) CustomToast.success(localizations.importSuccess).show(context);\n    } catch (e) {\n      if (mounted) CustomToast.error(localizations.importFailed).show(context);\n    }\n  }\n\n  Future<void> _export(List<RequestBreakpointRule> exportRules) async {\n    if (exportRules.isEmpty) return;\n\n    String? outputFile;\n    if (Platform.isMacOS) {\n      outputFile = await DesktopMultiWindow.invokeMethod(0, \"saveFile\", {\"fileName\": 'request_breakpoint_rules.json'});\n      if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show();\n    } else {\n      outputFile = await FilePicker.platform.saveFile(fileName: 'request_breakpoint_rules.json');\n    }\n    if (outputFile == null) return;\n    File file = File(outputFile);\n    try {\n      var json = exportRules.map((e) => e.toJson()).toList();\n      await file.writeAsString(jsonEncode(json));\n      if (mounted) CustomToast.success(localizations.exportSuccess).show(context);\n    } catch (e) {\n      if (mounted) CustomToast.error(localizations.exportFailed).show(context);\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    enabled = manager.enabled;\n    rules = manager.list;\n    HardwareKeyboard.instance.addHandler(onKeyEvent);\n  }\n\n  @override\n  void dispose() {\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if (HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.escape) && Navigator.canPop(context)) {\n      Navigator.maybePop(context);\n      return true;\n    }\n\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      if (Navigator.canPop(context)) {\n        Navigator.pop(context);\n        return true;\n      }\n      if (widget.windowId != null) {\n        WindowController.fromWindowId(widget.windowId!).close();\n      }\n      return true;\n    }\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isEN = Localizations.localeOf(context).languageCode == 'en';\n\n    return Scaffold(\n        backgroundColor: Theme.of(context).dialogTheme.backgroundColor,\n        appBar: AppBar(\n            title: Text(localizations.breakpoint, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n            toolbarHeight: 36,\n            centerTitle: true),\n        body: Center(\n            child: Container(\n                padding: const EdgeInsets.only(left: 15, right: 10),\n                child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n                  Row(children: [\n                    SizedBox(\n                        width: isEN ? 280 : 250,\n                        child: ListTile(\n                            title: Text(\"${localizations.enable} ${localizations.breakpoint}\"),\n                            contentPadding: const EdgeInsets.only(left: 2),\n                            trailing: SwitchWidget(\n                                value: enabled,\n                                scale: 0.8,\n                                onChanged: (val) async {\n                                  manager.enabled = val;\n                                  await _save();\n                                  enabled = val;\n                                }))),\n                    const SizedBox(width: 10),\n                    Expanded(\n                        child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n                      TextButton.icon(\n                          icon: const Icon(Icons.add, size: 18), label: Text(localizations.add), onPressed: _editRule),\n                      const SizedBox(width: 5),\n                      TextButton.icon(\n                          icon: const Icon(Icons.input_rounded, size: 18),\n                          onPressed: _import,\n                          label: Text(localizations.import)),\n                    ])),\n                    const SizedBox(width: 15)\n                  ]),\n                  const SizedBox(height: 10),\n                  Expanded(child: _buildList())\n                ]))));\n  }\n\n  Widget _buildList() {\n    return GestureDetector(\n      onSecondaryTap: () {\n        if (lastPressPosition == null) {\n          return;\n        }\n        _showMenu(lastPressPosition!);\n      },\n      onTapDown: (details) {\n        if (selected.isEmpty) {\n          return;\n        }\n        if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n          return;\n        }\n        setState(() {\n          selected.clear();\n        });\n      },\n      child: Listener(\n        onPointerUp: (event) => isPressed = false,\n        onPointerDown: (event) {\n          lastPressPosition = event.localPosition;\n          if (event.buttons == kPrimaryMouseButton) {\n            isPressed = true;\n          }\n        },\n        child: Container(\n          padding: const EdgeInsets.only(top: 10),\n          decoration: BoxDecoration(border: Border.all(color: Colors.grey.withValues(alpha: 0.2))),\n          child: Column(\n            children: [\n              Padding(\n                padding: const EdgeInsets.only(left: 5, bottom: 5),\n                child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [\n                  Container(width: 150, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)),\n                  SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)),\n                  const VerticalDivider(width: 10),\n                  Expanded(child: Text(\"URL\", textAlign: TextAlign.center)),\n                  SizedBox(width: 100, child: Text(localizations.breakpoint, textAlign: TextAlign.center)),\n                ]),\n              ),\n              const Divider(thickness: 0.5, height: 5),\n              Expanded(\n                child: ListView.builder(\n                  itemCount: rules.length,\n                  itemBuilder: (context, index) => _buildRow(index),\n                ),\n              )\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildRow(int index) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n    var rule = rules[index];\n\n    return InkWell(\n      highlightColor: Colors.transparent,\n      splashColor: Colors.transparent,\n      hoverColor: primaryColor.withValues(alpha: 0.3),\n      onDoubleTap: () => _editRule(rule: rule),\n      onSecondaryTapDown: (details) => _showMenu(details.globalPosition, index: index),\n      onHover: (hover) {\n        if (isPressed && !selected.contains(index)) {\n          setState(() {\n            selected.add(index);\n          });\n        }\n      },\n      onTap: () {\n        if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n          setState(() {\n            selected.contains(index) ? selected.remove(index) : selected.add(index);\n          });\n          return;\n        }\n        if (selected.isEmpty) {\n          return;\n        }\n        setState(() {\n          selected.clear();\n        });\n      },\n      child: Container(\n        color: selected.contains(index)\n            ? primaryColor.withValues(alpha: 0.5)\n            : index.isEven\n                ? Colors.grey.withValues(alpha: 0.1)\n                : null,\n        height: 32,\n        padding: const EdgeInsets.all(5),\n        child: Row(children: [\n          SizedBox(\n            width: 150,\n            child: Text(rule.name ?? \"\",\n                overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),\n          ),\n          SizedBox(\n              width: 50,\n              child: SwitchWidget(\n                  scale: 0.65,\n                  value: rule.enabled,\n                  onChanged: (val) async {\n                    rule.enabled = val;\n                    await _save();\n                  })),\n          const SizedBox(width: 10),\n          Expanded(child: Text(rule.url, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center)),\n          SizedBox(\n              width: 100,\n              child: Text(\n                  \"${rule.interceptRequest ? localizations.request : \"\"}${rule.interceptRequest && rule.interceptResponse ? \"/\" : \"\"}${rule.interceptResponse ? localizations.response : \"\"}\",\n                  textAlign: TextAlign.center,\n                  overflow: TextOverflow.ellipsis)),\n        ]),\n      ),\n    );\n  }\n\n  void _showMenu(Offset position, {int? index}) {\n    if (index != null) {\n      if (!selected.contains(index)) {\n        setState(() {\n          selected.clear();\n          selected.add(index);\n        });\n      }\n    }\n\n    showContextMenu(context, position, items: [\n      PopupMenuItem(\n        height: 32,\n        child: Text(localizations.edit),\n        onTap: () {\n          if (selected.length == 1) {\n            _editRule(rule: rules[selected.first]);\n          }\n        },\n      ),\n      PopupMenuItem(\n        height: 32,\n        child: Text(localizations.export),\n        onTap: () async {\n          if (selected.isEmpty) return;\n          var list = selected.toList();\n          List<RequestBreakpointRule> exportRules = [];\n          for (var i in list) {\n            exportRules.add(rules[i]);\n          }\n          await _export(exportRules);\n          setState(() {\n            selected.clear();\n          });\n        },\n      ),\n      PopupMenuItem(\n        height: 32,\n        child: Text(localizations.delete),\n        onTap: () async {\n          if (selected.isEmpty) return;\n          var list = selected.toList();\n          list.sort((a, b) => b.compareTo(a)); // Remove from end to avoid index shift issues\n          for (var i in list) {\n            rules.removeAt(i);\n          }\n          setState(() {\n            selected.clear();\n          });\n          await _save();\n        },\n      ),\n    ]);\n  }\n\n  void _editRule({RequestBreakpointRule? rule}) {\n    showDialog(\n      context: context,\n      builder: (context) => InterceptRuleDialog(rule: rule),\n    ).then((value) async {\n      if (value != null && value is RequestBreakpointRule) {\n        setState(() {\n          if (rule == null) {\n            rules.add(value);\n          }\n        });\n        await _save();\n      }\n    });\n  }\n}\n\nclass InterceptRuleDialog extends StatefulWidget {\n  final RequestBreakpointRule? rule;\n\n  const InterceptRuleDialog({super.key, this.rule});\n\n  @override\n  State<InterceptRuleDialog> createState() => _InterceptRuleDialogState();\n}\n\nclass _InterceptRuleDialogState extends State<InterceptRuleDialog> {\n  late RequestBreakpointRule rule;\n  final _formKey = GlobalKey<FormState>();\n\n  late TextEditingController nameInput;\n  late TextEditingController urlInput;\n\n  // Local state for methods to avoid modifying rule in-place before save\n  HttpMethod? _method;\n  bool _interceptRequest = true;\n  bool _interceptResponse = true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    rule = widget.rule ?? RequestBreakpointRule(url: '');\n    nameInput = TextEditingController(text: rule.name);\n    urlInput = TextEditingController(text: rule.url);\n    _method = rule.method;\n    _interceptRequest = rule.interceptRequest;\n    _interceptResponse = rule.interceptResponse;\n  }\n\n  InputDecoration decoration(String label, {String? hintText}) {\n    return InputDecoration(\n        floatingLabelBehavior: FloatingLabelBehavior.always,\n        labelText: label,\n        hintText: hintText,\n        isDense: true,\n        border: const OutlineInputBorder());\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n      title: Text(\n          widget.rule == null\n              ? \"${localizations.add} ${localizations.breakpointRule}\"\n              : \"${localizations.edit} ${localizations.breakpointRule}\",\n          style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),\n      actionsPadding: const EdgeInsets.only(right: 15, bottom: 15),\n      contentPadding: const EdgeInsets.only(left: 20, right: 20, top: 15, bottom: 15),\n      content: Container(\n        constraints: const BoxConstraints(minWidth: 350, maxWidth: 500),\n        child: SingleChildScrollView(\n          child: Form(\n            key: _formKey,\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Row(children: [\n                  SizedBox(width: 55, child: Text('${localizations.enable}:')),\n                  SwitchWidget(value: rule.enabled, onChanged: (val) => rule.enabled = val, scale: 0.8)\n                ]),\n                const SizedBox(height: 5),\n                textField('${localizations.name}:', nameInput, localizations.pleaseEnter),\n                const SizedBox(height: 10),\n                Row(children: [\n                  SizedBox(width: 60, child: Text('URL:')),\n                  Expanded(\n                    child: TextFormField(\n                      controller: urlInput,\n                      style: const TextStyle(fontSize: 14),\n                      validator: (val) => val?.isNotEmpty == true ? null : localizations.cannotBeEmpty,\n                      decoration: InputDecoration(\n                        hintText: 'https://www.example.com/api/*',\n                        hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n                        contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10),\n                        errorStyle: const TextStyle(height: 0, fontSize: 0),\n                        focusedBorder: focusedBorder(),\n                        isDense: true,\n                        border: const OutlineInputBorder(),\n                        prefixIcon: Padding(\n                          padding: const EdgeInsets.only(left: 6, right: 6),\n                          child: MethodPopupMenu(\n                            value: _method,\n                            showSeparator: true,\n                            onChanged: (val) {\n                              setState(() {\n                                _method = val;\n                              });\n                            },\n                          ),\n                        ),\n                      ),\n                    ),\n                  ),\n                ]),\n                const SizedBox(height: 10),\n                Text(localizations.breakpoint, style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 13)),\n                const SizedBox(height: 5),\n                Container(\n                  decoration: BoxDecoration(\n                      // border: Border.all(color: Colors.grey.withValues(alpha: 0.5)),\n                      borderRadius: BorderRadius.circular(5)),\n                  child: Row(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      Expanded(\n                          child: CheckboxListTile(\n                        contentPadding: const EdgeInsets.only(left: 10),\n                        title: Text(localizations.request, style: const TextStyle(fontSize: 14)),\n                        value: _interceptRequest,\n                        controlAffinity: ListTileControlAffinity.leading,\n                        dense: true,\n                        visualDensity: const VisualDensity(vertical: -4),\n                        onChanged: (val) {\n                          setState(() {\n                            _interceptRequest = val!;\n                          });\n                        },\n                      )),\n                      // Container(height: 30, width: 0.5, color: Colors.grey.withValues(alpha: 0.5)),\n                      Expanded(\n                          child: CheckboxListTile(\n                        contentPadding: const EdgeInsets.only(left: 10),\n                        title: Text(localizations.response, style: const TextStyle(fontSize: 14)),\n                        value: _interceptResponse,\n                        controlAffinity: ListTileControlAffinity.leading,\n                        dense: true,\n                        visualDensity: const VisualDensity(vertical: -4),\n                        onChanged: (val) {\n                          setState(() {\n                            _interceptResponse = val!;\n                          });\n                        },\n                      )),\n                      Expanded(child: SizedBox()),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n      actions: [\n        TextButton(\n          onPressed: () => Navigator.pop(context),\n          child: Text(localizations.cancel),\n        ),\n        FilledButton(\n          onPressed: () {\n            if (!(_formKey.currentState?.validate() ?? false)) {\n              CustomToast.error(\"URL ${localizations.cannotBeEmpty}\").show(context, alignment: Alignment.topCenter);\n              return;\n            }\n\n            rule.name = nameInput.text;\n            rule.url = urlInput.text;\n            rule.method = _method;\n            rule.interceptRequest = _interceptRequest;\n            rule.interceptResponse = _interceptResponse;\n            Navigator.pop(context, rule);\n          },\n          child: Text(localizations.save),\n        ),\n      ],\n    );\n  }\n\n  Widget textField(String label, TextEditingController controller, String hint,\n      {bool required = false, FormFieldSetter<String>? onSaved}) {\n    return Row(children: [\n      SizedBox(width: 60, child: Text(label)),\n      Expanded(\n          child: TextFormField(\n        controller: controller,\n        style: const TextStyle(fontSize: 14),\n        validator: (val) => val?.isNotEmpty == true || !required ? null : \"\",\n        onSaved: onSaved,\n        decoration: InputDecoration(\n            hintText: hint,\n            hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n            contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),\n            errorStyle: const TextStyle(height: 0, fontSize: 0),\n            focusedBorder: focusedBorder(),\n            isDense: true,\n            border: const OutlineInputBorder()),\n      ))\n    ]);\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/request_crypto.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\nimport 'dart:math' as math;\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_crypto_manager.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\nbool _refresh = false;\n\n/// 刷新配置\nFuture<void> _refreshConfig({bool force = false}) async {\n  if (force) {\n    _refresh = false;\n    await RequestCryptoManager.instance.then((manager) => manager.flushConfig());\n    await DesktopMultiWindow.invokeMethod(0, \"refreshRequestCrypto\");\n    return;\n  }\n\n  if (_refresh) {\n    return;\n  }\n  _refresh = true;\n  Future.delayed(const Duration(milliseconds: 1000), () async {\n    _refresh = false;\n    await RequestCryptoManager.instance.then((manager) => manager.flushConfig());\n    await DesktopMultiWindow.invokeMethod(0, \"refreshRequestCrypto\");\n  });\n}\n\nclass RequestCryptoPage extends StatefulWidget {\n  final int? windowId;\n  final RequestCryptoManager manager;\n\n  const RequestCryptoPage({super.key, this.windowId, required this.manager});\n\n  @override\n  State<RequestCryptoPage> createState() => _RequestCryptoPageState();\n}\n\nclass _RequestCryptoPageState extends State<RequestCryptoPage> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  RequestCryptoManager get manager => widget.manager;\n\n  @override\n  void initState() {\n    super.initState();\n    HardwareKeyboard.instance.addHandler(_onKeyEvent);\n  }\n\n  @override\n  void dispose() {\n    HardwareKeyboard.instance.removeHandler(_onKeyEvent);\n    super.dispose();\n  }\n\n  bool _onKeyEvent(KeyEvent event) {\n    if (HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.escape) && Navigator.canPop(context)) {\n      Navigator.maybePop(context);\n      return true;\n    }\n\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      if (Navigator.canPop(context)) {\n        Navigator.pop(context);\n        return true;\n      }\n      if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).close();\n      return true;\n    }\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isEN = Localizations.localeOf(context).languageCode == 'en';\n    return Scaffold(\n        backgroundColor: Theme.of(context).dialogTheme.backgroundColor,\n        appBar: AppBar(\n            title: Text(localizations.requestCrypto, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n            toolbarHeight: 36,\n            centerTitle: true),\n        body: Center(\n            child: Container(\n                padding: const EdgeInsets.only(left: 15, right: 10),\n                child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n                  Row(children: [\n                    SizedBox(\n                        width: isEN ? 310 : 225,\n                        child: ListTile(\n                            title: Text(\"${localizations.enable} ${localizations.requestCrypto}\"),\n                            trailing: SwitchWidget(\n                                value: manager.enabled,\n                                scale: 0.8,\n                                onChanged: (value) {\n                                  manager.enabled = value;\n                                  _refreshConfig();\n                                }))),\n                    const SizedBox(width: 10),\n                    Expanded(\n                        child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n                      TextButton.icon(\n                          icon: const Icon(Icons.add, size: 18), label: Text(localizations.add), onPressed: _addRule),\n                      const SizedBox(width: 5),\n                      TextButton.icon(\n                          icon: const Icon(Icons.input_rounded, size: 18),\n                          onPressed: _import,\n                          label: Text(localizations.import))\n                    ])),\n                    const SizedBox(width: 15)\n                  ]),\n                  const SizedBox(height: 16),\n                  CryptoRuleList(manager: manager, windowId: widget.windowId),\n                ]))));\n  }\n\n  Future<void> _addRule() async {\n    final newRule =\n        await showDialog<CryptoRule>(context: context, barrierDismissible: false, builder: (_) => CryptoRuleDialog());\n    if (newRule == null) return;\n    await manager.addRule(newRule);\n    setState(() {});\n    _refreshConfig(force: true);\n  }\n\n  Future<void> _import() async {\n    String? path;\n    if (Platform.isMacOS) {\n      path = await DesktopMultiWindow.invokeMethod(0, \"pickFiles\", {\n        \"allowedExtensions\": ['json']\n      });\n      if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show();\n    } else {\n      FilePickerResult? result =\n          await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']);\n      path = result?.files.single.path;\n    }\n    if (path == null) return;\n    try {\n      final content = await File(path).readAsString();\n      final List list = jsonDecode(content);\n      for (final item in list) {\n        await manager.addRule(CryptoRule.fromJson(Map<String, dynamic>.from(item)));\n      }\n      _refreshConfig(force: true);\n      if (mounted) FlutterToastr.show(localizations.importSuccess, context);\n    } catch (e) {\n      logger.e('导入失败 $path', error: e);\n      if (mounted) FlutterToastr.show('${localizations.importFailed} $e', context);\n    }\n  }\n}\n\n// Reusable rule list component extracted from _RequestCryptoPageState\nclass CryptoRuleList extends StatefulWidget {\n  final int? windowId;\n  final RequestCryptoManager manager;\n\n  const CryptoRuleList({\n    required this.manager,\n    super.key,\n    this.windowId,\n  });\n\n  @override\n  State<CryptoRuleList> createState() => _CryptoRuleListState();\n}\n\nclass _CryptoRuleListState extends State<CryptoRuleList> {\n  RequestCryptoManager get manager => widget.manager;\n  Set<int> selected = {};\n  bool isPressed = false;\n  Offset? lastPressPosition;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onSecondaryTap: () {\n        if (lastPressPosition == null) {\n          return;\n        }\n        showGlobalMenu(lastPressPosition!);\n      },\n      onTapDown: (details) {\n        if (selected.isEmpty) {\n          return;\n        }\n        if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n          return;\n        }\n        setState(() {\n          selected.clear();\n        });\n      },\n      child: Listener(\n        onPointerUp: (event) => isPressed = false,\n        onPointerDown: (event) {\n          lastPressPosition = event.localPosition;\n          if (event.buttons == kPrimaryMouseButton) {\n            isPressed = true;\n          }\n        },\n        child: Container(\n          padding: const EdgeInsets.only(top: 10),\n          constraints: const BoxConstraints(minHeight: 200, maxHeight: 600),\n          decoration: BoxDecoration(border: Border.all(color: Colors.grey.withAlpha((0.2 * 255).round()))),\n          child: Column(\n            children: [\n              Padding(\n                padding: const EdgeInsets.only(left: 5, bottom: 5),\n                child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [\n                  Container(width: 80, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)),\n                  SizedBox(width: 80, child: Text(localizations.enable, textAlign: TextAlign.center)),\n                  const VerticalDivider(width: 24),\n                  const Expanded(child: Text('URL', textAlign: TextAlign.center)),\n                  SizedBox(width: 120, child: Text(localizations.cryptoRuleField, textAlign: TextAlign.center)),\n                  SizedBox(width: 220, child: Text('AES Key', textAlign: TextAlign.center)),\n                ]),\n              ),\n              const Divider(thickness: 0.5, height: 5),\n              Column(children: rows(manager.rules))\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  List<Widget> rows(List<CryptoRule> rules) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n\n    return List.generate(rules.length, (index) {\n      final rule = rules[index];\n      return InkWell(\n        highlightColor: Colors.transparent,\n        splashColor: Colors.transparent,\n        hoverColor: primaryColor.withOpacity(0.3),\n        onDoubleTap: () => showEdit(index),\n        onSecondaryTapDown: (details) => showMenus(details, index),\n        onHover: (hover) {\n          if (isPressed && !selected.contains(index)) {\n            setState(() {\n              selected.add(index);\n            });\n          }\n        },\n        onTap: () {\n          if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n            setState(() {\n              selected.contains(index) ? selected.remove(index) : selected.add(index);\n            });\n            return;\n          }\n          if (selected.isEmpty) {\n            return;\n          }\n          setState(() {\n            selected.clear();\n          });\n        },\n        child: Container(\n          color: selected.contains(index)\n              ? primaryColor.withOpacity(0.6)\n              : index.isEven\n                  ? Colors.grey.withOpacity(0.1)\n                  : null,\n          height: 32,\n          padding: const EdgeInsets.all(5),\n          child: Row(children: [\n            SizedBox(\n              width: 80,\n              child: Text(rule.name,\n                  overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),\n            ),\n            SizedBox(\n                width: 80,\n                child: SwitchWidget(\n                    scale: 0.7,\n                    value: rule.enabled,\n                    onChanged: (val) {\n                      rules[index].enabled = val;\n                      _refreshConfig();\n                    })),\n            const SizedBox(width: 8),\n            Expanded(\n                child: Text(rule.urlPattern.isEmpty ? localizations.emptyMatchAll : rule.urlPattern,\n                    overflow: TextOverflow.ellipsis)),\n            SizedBox(\n                width: 120,\n                child: Text(rule.field ?? '', overflow: TextOverflow.ellipsis, textAlign: TextAlign.center)),\n            SizedBox(\n                width: 220,\n                child: Text(_formatKey(rule.config.key), overflow: TextOverflow.ellipsis, textAlign: TextAlign.center)),\n          ]),\n        ),\n      );\n    });\n  }\n\n  Future<void> showEdit([int? index]) async {\n    final rule = index == null ? null : manager.rules[index];\n    if (!mounted) {\n      return;\n    }\n\n    final updated = await showDialog<CryptoRule>(context: context, builder: (_) => CryptoRuleDialog(rule: rule));\n    if (updated == null) return;\n    if (index == null) {\n      await manager.addRule(updated);\n    } else {\n      await manager.updateRule(index, updated);\n    }\n    _refreshConfig(force: true);\n    setState(() {});\n  }\n\n  Future<void> removeRules(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    showConfirmDialog(context, content: localizations.confirmContent, onConfirm: () async {\n      indexes.sort((a, b) => b.compareTo(a));\n      for (final index in indexes) {\n        await manager.removeRule(index);\n      }\n      selected.clear();\n      _refreshConfig(force: true);\n    });\n  }\n\n  void showMenus(TapDownDetails details, int index) {\n    if (selected.length > 1) {\n      showGlobalMenu(details.globalPosition);\n      return;\n    }\n    setState(() {\n      selected.add(index);\n    });\n\n    showContextMenu(context, details.globalPosition, items: [\n      PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)),\n      PopupMenuItem(height: 35, child: Text(localizations.delete), onTap: () => removeRules([index]))\n    ]);\n  }\n\n  void showGlobalMenu(Offset offset) {\n    showContextMenu(context, offset, items: [\n      PopupMenuItem(height: 35, onTap: showEdit, child: Text(localizations.newBuilt)),\n      PopupMenuItem(height: 35, child: Text(localizations.export), onTap: () => export(selected.toList())),\n      const PopupMenuDivider(),\n      PopupMenuItem(height: 35, child: Text(localizations.enableSelect), onTap: () => enableStatus(true)),\n      PopupMenuItem(height: 35, child: Text(localizations.disableSelect), onTap: () => enableStatus(false)),\n      const PopupMenuDivider(),\n      PopupMenuItem(height: 35, child: Text(localizations.deleteSelect), onTap: () => removeRules(selected.toList()))\n    ]);\n  }\n\n  Future<void> enableStatus(bool enable) async {\n    if (selected.isEmpty) return;\n    for (final entry in selected) {\n      manager.rules[entry].enabled = enable;\n    }\n    setState(() {});\n    _refreshConfig(force: true);\n  }\n\n  Future<void> export(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    indexes.sort();\n    final data = indexes.map((i) => manager.rules[i].toJson()).toList();\n    String? path;\n    if (Platform.isMacOS) {\n      path = await DesktopMultiWindow.invokeMethod(0, \"saveFile\", {\"fileName\": 'request_crypto.json'});\n      if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show();\n    } else {\n      path = await FilePicker.platform.saveFile(fileName: 'request_crypto.json');\n    }\n    if (path == null) return;\n    await File(path).writeAsString(jsonEncode(data));\n    if (mounted) FlutterToastr.show(localizations.exportSuccess, context);\n  }\n\n  // Format AES key for display: strip optional 'base64:' prefix and truncate long values\n  String _formatKey(String? raw) {\n    if (raw == null || raw.trim().isEmpty) return '';\n    var k = raw.trim();\n    if (k.startsWith('base64:')) {\n      k = k.substring(7);\n    }\n    if (k.length > 40) return '${k.substring(0, 40)}...';\n    return k;\n  }\n}\n\nclass CryptoRuleDialog extends StatefulWidget {\n  final CryptoRule? rule;\n\n  const CryptoRuleDialog({super.key, this.rule});\n\n  @override\n  State<CryptoRuleDialog> createState() => _CryptoRuleDialogState();\n}\n\nclass _CryptoRuleDialogState extends State<CryptoRuleDialog> {\n  late TextEditingController nameController;\n  late TextEditingController patternController;\n  late TextEditingController keyController;\n  late TextEditingController ivController;\n  late TextEditingController fieldInputController;\n  String mode = 'CBC';\n  String padding = 'PKCS7';\n  int length = 128;\n  bool enabled = true;\n\n  // single field support\n  late String fieldItem;\n  final _formKey = GlobalKey<FormState>();\n  String keyFormat = 'text';\n  String ivSource = 'manual';\n  int ivPrefixLength = 16;\n\n  @override\n  void initState() {\n    super.initState();\n    final rule = widget.rule;\n    nameController = TextEditingController(text: rule?.name ?? '');\n    patternController = TextEditingController(text: rule?.urlPattern ?? '');\n    keyController = TextEditingController(text: rule?.config.key);\n    ivController = TextEditingController(text: rule?.config.iv);\n    // single field support: initialize from first existing field if present\n    fieldInputController = TextEditingController(text: rule?.field ?? '');\n    mode = rule?.config.mode ?? 'CBC';\n    padding = rule?.config.padding ?? 'PKCS7';\n    length = rule?.config.keyLength ?? 256;\n    enabled = rule?.enabled ?? true;\n    fieldItem = rule?.field ?? '';\n    // detect stored key/iv prefix (support base64: or plain text)\n    final storedKey = rule?.config.key ?? '';\n    if (storedKey.startsWith('base64:')) {\n      keyFormat = 'base64';\n      keyController.text = storedKey.substring(7);\n    } else {\n      keyFormat = 'text';\n      keyController.text = storedKey;\n    }\n\n    final storedIv = rule?.config.iv ?? '';\n    // keep stored iv as-is if prefixed with base64:, otherwise show raw value\n    if (storedIv.startsWith('base64:')) {\n      ivController.text = storedIv.substring(7);\n    } else {\n      ivController.text = storedIv;\n    }\n    // iv source and prefix length\n    ivSource = rule?.config.ivSource ?? 'manual';\n    ivPrefixLength = rule?.config.ivPrefixLength ?? 16;\n  }\n\n  @override\n  void dispose() {\n    nameController.dispose();\n    patternController.dispose();\n    keyController.dispose();\n    ivController.dispose();\n    fieldInputController.dispose();\n    super.dispose();\n  }\n\n  InputDecoration decorate(BuildContext context, String? label, {String? hint, Widget? suffixIcon}) {\n    return InputDecoration(\n        floatingLabelBehavior: FloatingLabelBehavior.always,\n        labelText: label,\n        hintText: hint,\n        hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 15),\n        isDense: true,\n        border: const OutlineInputBorder());\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final l10n = AppLocalizations.of(context)!;\n    final theme = Theme.of(context);\n\n    return AlertDialog(\n      title: Text(widget.rule == null ? l10n.newBuilt : l10n.edit),\n      scrollable: true,\n      titlePadding: const EdgeInsets.only(top: 10, left: 20),\n      actionsPadding: const EdgeInsets.only(right: 15, bottom: 15),\n      contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),\n      content: Container(\n        width: 550,\n        constraints: const BoxConstraints(minHeight: 200, maxHeight: 560),\n        child: Form(\n          key: _formKey,\n          child: SingleChildScrollView(\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Card(\n                  color: Theme.of(context).colorScheme.surfaceContainerLow.withAlpha((0.5 * 255).round()),\n                  elevation: 0,\n                  shape: RoundedRectangleBorder(\n                    side: BorderSide(color: Theme.of(context).dividerColor.withAlpha((0.2 * 255).round())),\n                    borderRadius: BorderRadius.circular(8),\n                  ),\n                  child: Padding(\n                    padding: const EdgeInsets.all(10),\n                    child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n                      Text(l10n.match, style: theme.textTheme.titleSmall),\n                      const SizedBox(height: 12),\n                      TextFormField(controller: nameController, decoration: decorate(context, l10n.name)),\n                      const SizedBox(height: 12),\n                      TextFormField(\n                        controller: patternController,\n                        decoration: decorate(context, \"URL\", hint: 'https://www.example.com/api/*'),\n                        validator: (val) => val == null || val.trim().isEmpty ? l10n.cannotBeEmpty : null,\n                      ),\n                      const SizedBox(height: 12),\n                      TextFormField(\n                        controller: fieldInputController,\n                        decoration: decorate(context, l10n.cryptoRuleField, hint: 'data.field'),\n                      ),\n                      const SizedBox(height: 12),\n                      SwitchListTile(\n                        dense: true,\n                        contentPadding: EdgeInsets.zero,\n                        title: Text(l10n.enable),\n                        value: enabled,\n                        onChanged: (value) => setState(() => enabled = value),\n                      ),\n                    ]),\n                  ),\n                ),\n                const SizedBox(height: 12),\n                Card(\n                  color: Theme.of(context).colorScheme.surfaceContainerLow.withAlpha((0.5 * 255).round()),\n                  elevation: 0,\n                  shape: RoundedRectangleBorder(\n                    side: BorderSide(color: Theme.of(context).dividerColor.withAlpha((0.2 * 255).round())),\n                    borderRadius: BorderRadius.circular(8),\n                  ),\n                  child: Padding(\n                    padding: const EdgeInsets.all(10),\n                    child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n                      Text(\"AES\", style: theme.textTheme.titleSmall),\n                      const SizedBox(height: 12),\n                      Row(children: [\n                        Text(\"Mode\", style: theme.textTheme.labelMedium),\n                        const SizedBox(width: 8),\n                        Container(\n                          height: 42,\n                          padding: const EdgeInsets.symmetric(horizontal: 8),\n                          decoration: BoxDecoration(\n                            border: Border.all(color: Theme.of(context).dividerColor.withAlpha((0.12 * 255).round())),\n                            borderRadius: BorderRadius.circular(6),\n                          ),\n                          child: DropdownButtonHideUnderline(\n                            child: DropdownButton<String>(\n                              value: mode,\n                              items: const [\n                                DropdownMenuItem(value: 'ECB', child: Text('ECB')),\n                                DropdownMenuItem(value: 'CBC', child: Text('CBC')),\n                              ],\n                              onChanged: (v) => setState(() => mode = v ?? 'ECB'),\n                              style: Theme.of(context).textTheme.bodyMedium,\n                            ),\n                          ),\n                        ),\n                        const SizedBox(width: 12),\n                        Text('Padding', style: theme.textTheme.labelMedium),\n                        const SizedBox(width: 8),\n                        Container(\n                          height: 42,\n                          padding: const EdgeInsets.symmetric(horizontal: 8),\n                          decoration: BoxDecoration(\n                            border: Border.all(color: Theme.of(context).dividerColor.withAlpha((0.12 * 255).round())),\n                            borderRadius: BorderRadius.circular(6),\n                          ),\n                          child: DropdownButtonHideUnderline(\n                            child: DropdownButton<String>(\n                              value: padding,\n                              items: const [\n                                DropdownMenuItem(value: 'PKCS7', child: Text('PKCS7')),\n                                DropdownMenuItem(value: 'ZeroPadding', child: Text('ZeroPadding')),\n                              ],\n                              onChanged: (v) => setState(() => padding = v ?? 'PKCS7'),\n                              style: Theme.of(context).textTheme.bodyMedium,\n                            ),\n                          ),\n                        ),\n                        const SizedBox(width: 12),\n                        Text('Key Length', style: theme.textTheme.labelMedium),\n                        const SizedBox(width: 8),\n                        Container(\n                          height: 42,\n                          padding: const EdgeInsets.symmetric(horizontal: 8),\n                          decoration: BoxDecoration(\n                            border: Border.all(color: Theme.of(context).dividerColor.withAlpha((0.12 * 255).round())),\n                            borderRadius: BorderRadius.circular(6),\n                          ),\n                          child: DropdownButtonHideUnderline(\n                            child: DropdownButton<int>(\n                              value: length,\n                              items: const [\n                                DropdownMenuItem(value: 128, child: Text('128')),\n                                DropdownMenuItem(value: 192, child: Text('192')),\n                                DropdownMenuItem(value: 256, child: Text('256')),\n                              ],\n                              onChanged: (v) => setState(() => length = v ?? 128),\n                              style: Theme.of(context).textTheme.bodyMedium,\n                            ),\n                          ),\n                        ),\n                      ]),\n                      const SizedBox(height: 12),\n                      // Key input and format selector in a single row for nicer UI\n                      Row(children: [\n                        Container(\n                          height: 42,\n                          width: 92,\n                          padding: const EdgeInsets.symmetric(horizontal: 6),\n                          decoration: BoxDecoration(\n                            border: Border.all(color: Theme.of(context).dividerColor.withAlpha((0.12 * 255).round())),\n                            borderRadius: BorderRadius.circular(6),\n                          ),\n                          child: DropdownButtonHideUnderline(\n                            child: DropdownButton<String>(\n                              value: keyFormat,\n                              items: const [\n                                DropdownMenuItem(value: 'text', child: Text('text')),\n                                DropdownMenuItem(value: 'base64', child: Text('base64')),\n                              ],\n                              onChanged: (v) => setState(() => keyFormat = v ?? 'text'),\n                              style: Theme.of(context).textTheme.bodyMedium,\n                              iconEnabledColor: Theme.of(context).colorScheme.primary,\n                            ),\n                          ),\n                        ),\n                        const SizedBox(width: 12),\n                        Expanded(\n                          child: SizedBox(\n                            child: TextFormField(\n                              controller: keyController,\n                              maxLength: 128,\n                              decoration: decorate(context, \"Key\").copyWith(counterText: ''),\n                              validator: (val) => val == null || val.trim().isEmpty ? l10n.cannotBeEmpty : null,\n                            ),\n                          ),\n                        ),\n                      ]),\n                      const SizedBox(height: 12),\n                      // Compact single-line IV controls for CBC\n                      if (mode == 'CBC')\n                        Row(children: [\n                          Container(\n                            height: 42,\n                            constraints:  const BoxConstraints(minWidth: 92),\n                            padding: const EdgeInsets.symmetric(horizontal: 6),\n                            decoration: BoxDecoration(\n                              border: Border.all(color: Theme.of(context).dividerColor.withAlpha((0.12 * 255).round())),\n                              borderRadius: BorderRadius.circular(6),\n                            ),\n                            child: DropdownButtonHideUnderline(\n                              child: DropdownButton<String>(\n                                value: ivSource,\n                                items: [\n                                  DropdownMenuItem(value: 'manual', child: Text(l10n.manual)),\n                                  DropdownMenuItem(value: 'prefix', child: Text(l10n.cryptoIvPrefixLabel)),\n                                ],\n                                onChanged: (v) => setState(() => ivSource = v ?? 'manual'),\n                                style: Theme.of(context).textTheme.bodyMedium,\n                                iconEnabledColor: Theme.of(context).colorScheme.primary,\n                              ),\n                            ),\n                          ),\n                          const SizedBox(width: 8),\n                          // narrow IV input when manual (fixed width for compactness)\n                          if (ivSource == 'manual')\n                            SizedBox(\n                              width: 260,\n                              height: 42,\n                              child: TextFormField(\n                                controller: ivController,\n                                decoration: decorate(context, 'IV').copyWith(\n                                    isDense: true,\n                                    contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10)),\n                                validator: (val) => (ivSource == 'manual' && (val == null || val.trim().isEmpty))\n                                    ? l10n.cannotBeEmpty\n                                    : null,\n                              ),\n                            ),\n                          if (ivSource == 'manual') const SizedBox(width: 8),\n                          if (ivSource == 'prefix')\n                            Tooltip(\n                                message: l10n.cryptoIvPrefixTooltip,\n                                child: Icon(Icons.info_outline, size: 16, color: theme.dividerColor)),\n                          if (ivSource == 'prefix') const SizedBox(width: 8),\n                          // compact numeric stepper (prefix length)\n                          if (ivSource == 'prefix')\n                            Container(\n                              decoration: BoxDecoration(\n                                  border: Border.all(color: theme.dividerColor.withAlpha(0x40)),\n                                  borderRadius: BorderRadius.circular(4)),\n                              child: Row(children: [\n                                IconButton(\n                                  padding: EdgeInsets.zero,\n                                  icon: const Icon(Icons.remove, size: 14),\n                                  onPressed: ivSource == 'prefix'\n                                      ? () => setState(() => ivPrefixLength = math.max(1, ivPrefixLength - 1))\n                                      : null,\n                                  constraints: const BoxConstraints.tightFor(width: 28, height: 28),\n                                ),\n                                SizedBox(\n                                    width: 36,\n                                    child: Center(\n                                        child: Text(ivPrefixLength.toString(), style: theme.textTheme.bodySmall))),\n                                IconButton(\n                                  padding: EdgeInsets.zero,\n                                  icon: const Icon(Icons.add, size: 14),\n                                  onPressed: ivSource == 'prefix'\n                                      ? () => setState(() => ivPrefixLength = math.min(1024, ivPrefixLength + 1))\n                                      : null,\n                                  constraints: const BoxConstraints.tightFor(width: 28, height: 28),\n                                ),\n                              ]),\n                            ),\n                        ]),\n                    ]),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n      actions: [\n        TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(l10n.cancel)),\n        FilledButton(\n          onPressed: () {\n            if (!(_formKey.currentState as FormState).validate()) return;\n            String outKey = keyController.text.trim();\n            // add prefix based on selected keyFormat if user did not already include explicit prefix\n            if (!(outKey.startsWith('base64:'))) {\n              if (keyFormat == 'base64') {\n                outKey = 'base64:$outKey';\n              }\n            }\n\n            // only set explicit IV when manual source is used\n            String outIv = '';\n            if (ivSource == 'manual') {\n              outIv = ivController.text.trim();\n              if (!(outIv.startsWith('base64:'))) {\n                if (keyFormat == 'base64') {\n                  outIv = 'base64:$outIv';\n                }\n              }\n            }\n\n            // save single field from the input controller\n            final savedField = fieldInputController.text.trim();\n            final updated = (widget.rule ?? CryptoRule.newRule()).copyWith(\n              name: nameController.text.trim(),\n              urlPattern: patternController.text.trim(),\n              field: savedField,\n              enabled: enabled,\n              config: CryptoKeyConfig(\n                  key: outKey,\n                  iv: outIv,\n                  ivSource: ivSource,\n                  ivPrefixLength: ivPrefixLength,\n                  mode: mode,\n                  padding: padding,\n                  keyLength: length),\n            );\n            Navigator.of(context).pop(updated);\n          },\n          child: Text(l10n.save),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/request_map/map_local.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:get/get.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_map_manager.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/ui/component/state_component.dart';\n\n/// 重写替换\n/// @author wanghongen\n/// 2023/10/8\nclass DesktopMapLocal extends StatefulWidget {\n  final int? windowId;\n  final RequestMapItem? item;\n\n  const DesktopMapLocal({super.key, this.item, this.windowId});\n\n  @override\n  State<DesktopMapLocal> createState() => MapLocaleState();\n}\n\nclass MapLocaleState extends State<DesktopMapLocal> {\n  final _headerKey = GlobalKey<HeadersState>();\n  final bodyTextController = TextEditingController();\n\n  RxString bodyType = RxString(ReplaceBodyType.text.name);\n  Rxn<String> bodyFile = Rxn<String>();\n  TextEditingController statusCodeController = TextEditingController(text: '200');\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  initState() {\n    super.initState();\n    initItem(widget.item);\n  }\n\n  @override\n  dispose() {\n    bodyTextController.dispose();\n    statusCodeController.dispose();\n    super.dispose();\n  }\n\n  ///初始化重写项\n  void initItem(RequestMapItem? item) {\n    if (item == null) {\n      return;\n    }\n    statusCodeController.text = item.statusCode?.toString() ?? '200';\n    bodyTextController.text = item.body ?? '';\n    bodyType.value = item.bodyType ?? ReplaceBodyType.text.name;\n    if (item.bodyType == ReplaceBodyType.file.name) {\n      bodyFile.value = item.bodyFile;\n    }\n  }\n\n  RequestMapItem getRequestMapItem() {\n    RequestMapItem item = widget.item ?? RequestMapItem();\n    var headers = _headerKey.currentState?.getHeaders() ?? widget.item?.headers;\n    item.statusCode = int.tryParse(statusCodeController.text) ?? 200;\n    item.headers = headers;\n    item.body = bodyTextController.text;\n    item.bodyType = bodyType.value;\n    if (item.bodyType == ReplaceBodyType.file.name) {\n      item.bodyFile = bodyFile.value;\n    } else {\n      item.bodyFile = null;\n    }\n    return item;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    List<String> tabs = [localizations.statusCode, localizations.responseHeader, localizations.responseBody];\n\n    return Container(\n      constraints: const BoxConstraints(maxHeight: 340),\n      child: DefaultTabController(\n          length: tabs.length,\n          initialIndex: tabs.length - 1,\n          child: Scaffold(\n            appBar: tabBar(tabs),\n            body: TabBarView(children: [\n              KeepAliveWrapper(child: statusCodeEdit()),\n              KeepAliveWrapper(child: headers()),\n              KeepAliveWrapper(child: body())\n            ]),\n          )),\n    );\n  }\n\n  //tabBar\n  TabBar tabBar(List<String> tabs) {\n    return TabBar(\n        tabs: tabs\n            .map((label) => Tab(\n                  height: 38,\n                  child: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n                ))\n            .toList());\n  }\n\n  //body\n  Widget body() {\n    bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en');\n\n    return Obx(() => Column(children: [\n          Row(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [\n            const SizedBox(width: 5),\n            Text(\"${localizations.type}: \"),\n            SizedBox(\n                width: 90,\n                child: DropdownButtonFormField<String>(\n                    value: bodyType.value,\n                    focusColor: Colors.transparent,\n                    itemHeight: 48,\n                    decoration: const InputDecoration(\n                        contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none),\n                    items: ReplaceBodyType.values\n                        .map((e) => DropdownMenuItem(\n                            value: e.name,\n                            child: Text(isEN ? e.name.toUpperCase() : e.label,\n                                style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500))))\n                        .toList(),\n                    onChanged: (val) => bodyType.value = val ?? ReplaceBodyType.text.name)),\n          ]),\n          const SizedBox(height: 10),\n          if (bodyType.value == ReplaceBodyType.file.name)\n            fileBodyEdit()\n          else\n            TextFormField(\n                controller: bodyTextController,\n                style: const TextStyle(fontSize: 14),\n                maxLines: 11,\n                decoration: decoration(localizations.replaceBodyWith,\n                    hintText: '${localizations.example} {\"code\":\"200\",\"data\":{}}')),\n        ]));\n  }\n\n  Widget fileBodyEdit() {\n    //选择文件  删除\n    return Obx(() => Row(crossAxisAlignment: CrossAxisAlignment.start, children: [\n          Expanded(\n              child: bodyFile.value == null\n                  ? Container(height: 50)\n                  : Container(\n                      padding: const EdgeInsets.all(5),\n                      foregroundDecoration:\n                          BoxDecoration(border: Border.all(color: Theme.of(context).colorScheme.primary, width: 1)),\n                      child: Text(bodyFile.value ?? ''))),\n          const SizedBox(width: 10),\n          FilledButton(\n              onPressed: () async {\n                String? path;\n                if (Platform.isMacOS) {\n                  path = await DesktopMultiWindow.invokeMethod(0, \"pickFiles\");\n                  if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show();\n                } else {\n                  FilePickerResult? result = await FilePicker.platform.pickFiles();\n                  path = result?.files.single.path;\n                }\n\n                if (path == null) {\n                  return;\n                }\n                bodyFile.value = path;\n              },\n              child: Text(localizations.selectFile, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500))),\n          const SizedBox(width: 10),\n          FilledButton(\n              onPressed: () {\n                setState(() {\n                  bodyFile.value = null;\n                });\n              },\n              child: Text(localizations.delete, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500))),\n        ]));\n  }\n\n  //headers\n  Widget headers() {\n    return Headers(headers: widget.item?.headers, key: _headerKey);\n  }\n\n  Widget textField(String label, dynamic value, String hint, {ValueChanged<String>? onChanged}) {\n    return Row(children: [\n      SizedBox(width: 80, child: Text(label)),\n      Expanded(\n          child: TextFormField(\n        initialValue: value,\n        onChanged: onChanged,\n        decoration: InputDecoration(\n            hintText: hint,\n            hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n            contentPadding: const EdgeInsets.all(10),\n            errorStyle: const TextStyle(height: 0, fontSize: 0),\n            focusedBorder: focusedBorder(),\n            isDense: true,\n            border: const OutlineInputBorder()),\n      ))\n    ]);\n  }\n\n  Widget statusCodeEdit() {\n    return Container(\n        padding: const EdgeInsets.all(10),\n        child: Column(children: [\n          Row(crossAxisAlignment: CrossAxisAlignment.center, children: [\n            Text(localizations.statusCode),\n            const SizedBox(width: 10),\n            SizedBox(\n                width: 100,\n                child: TextFormField(\n                  controller: statusCodeController,\n                  style: const TextStyle(fontSize: 14),\n                  inputFormatters: [FilteringTextInputFormatter.digitsOnly],\n                  decoration: InputDecoration(\n                      contentPadding: const EdgeInsets.all(10),\n                      focusedBorder: focusedBorder(),\n                      isDense: true,\n                      border: const OutlineInputBorder()),\n                )),\n            const SizedBox(width: 10),\n          ])\n        ]));\n  }\n\n  InputDecoration decoration(String label, {String? hintText}) {\n    Color color = Theme.of(context).colorScheme.primary;\n    // Color color = Colors.blueAccent;\n    return InputDecoration(\n        floatingLabelBehavior: FloatingLabelBehavior.always,\n        labelText: label,\n        hintStyle: TextStyle(color: Colors.grey.shade500),\n        hintText: hintText,\n        isDense: true,\n        border: OutlineInputBorder(borderSide: BorderSide(width: 0.8, color: color)),\n        enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.5, color: color)),\n        focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color)));\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n\n///请求头\nclass Headers extends StatefulWidget {\n  final Map<String, String>? headers;\n\n  const Headers({super.key, this.headers});\n\n  @override\n  State<StatefulWidget> createState() {\n    return HeadersState();\n  }\n}\n\nclass HeadersState extends State<Headers> with AutomaticKeepAliveClientMixin {\n  final Map<TextEditingController, TextEditingController> _headers = {};\n\n  @override\n  bool get wantKeepAlive => true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.headers == null) {\n      _headers[TextEditingController()] = TextEditingController();\n      return;\n    }\n\n    setHeaders(widget.headers);\n  }\n\n  void setHeaders(Map<String, String>? headers) {\n    _clear();\n    headers?.forEach((name, value) {\n      _headers[TextEditingController(text: name)] = TextEditingController(text: value);\n    });\n    _headers[TextEditingController()] = TextEditingController();\n  }\n\n  ///获取所有请求头\n  Map<String, String> getHeaders() {\n    var headers = <String, String>{};\n    _headers.forEach((name, value) {\n      if (name.text.isEmpty) {\n        return;\n      }\n      headers[name.text] = value.text;\n    });\n    return headers;\n  }\n\n  @override\n  dispose() {\n    _clear();\n    super.dispose();\n  }\n\n  void _clear() {\n    _headers.forEach((key, value) {\n      key.dispose();\n      value.dispose();\n    });\n    _headers.clear();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    var list = _buildRows();\n\n    return Column(children: [\n      Expanded(\n          child: Padding(\n              padding: const EdgeInsets.only(top: 10, bottom: 10),\n              child: ListView.separated(\n                  shrinkWrap: true,\n                  separatorBuilder: (context, index) =>\n                      index == list.length ? const SizedBox() : const Divider(thickness: 0.2),\n                  itemBuilder: (context, index) => list[index],\n                  itemCount: list.length))),\n      TextButton(\n        child: Text(\"${localizations.add}Header\", textAlign: TextAlign.center),\n        onPressed: () {\n          setState(() {\n            _headers[TextEditingController()] = TextEditingController();\n          });\n        },\n      ),\n    ]);\n  }\n\n  List<Widget> _buildRows() {\n    List<Widget> list = [];\n\n    _headers.forEach((key, val) {\n      list.add(_row(\n          _cell(key, isKey: true),\n          _cell(val),\n          Padding(\n              padding: const EdgeInsets.only(right: 15),\n              child: InkWell(\n                  onTap: () {\n                    setState(() {\n                      _headers.remove(key);\n                    });\n                  },\n                  child: const Icon(Icons.remove_circle_outline, size: 16)))));\n    });\n\n    return list;\n  }\n\n  Widget _cell(TextEditingController val, {bool isKey = false}) {\n    return Container(\n        padding: const EdgeInsets.only(right: 5),\n        child: TextFormField(\n            style: TextStyle(fontSize: 12, fontWeight: isKey ? FontWeight.w500 : null),\n            controller: val,\n            minLines: 1,\n            maxLines: 3,\n            decoration: InputDecoration(\n                isDense: true,\n                border: const OutlineInputBorder(),\n                enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 0.5, color: Colors.grey)),\n                hintStyle: TextStyle(fontSize: 12, color: Colors.grey),\n                hintText: isKey ? \"Key\" : \"Value\")));\n  }\n\n  Widget _row(Widget key, Widget val, Widget? op) {\n    return Row(children: [\n      Expanded(flex: 4, child: key),\n      const Text(\": \", style: TextStyle(color: Colors.deepOrangeAccent)),\n      Expanded(flex: 6, child: val),\n      op ?? const SizedBox()\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/request_map/map_scipt.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_code_editor/flutter_code_editor.dart';\nimport 'package:flutter_highlight/themes/monokai-sublime.dart';\nimport 'package:highlight/languages/javascript.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\n\nclass DesktopMapScript extends StatefulWidget {\n  final String? script;\n\n  const DesktopMapScript({super.key, this.script});\n\n  @override\n  State<StatefulWidget> createState() => MapScriptState();\n}\n\nclass MapScriptState extends State<DesktopMapScript> {\n  static String template = \"\"\"\nasync function onRequest(context, request) {\n  console.log(request.url);\n  //use fetch API request\n  // var result = await fetch('https://www.baidu.com/');\n  var response = {\n    statusCode: 200,\n    body: 'Hello, world!',\n    headers: {\n      'Content-Type': 'text/plain',\n      'X-My-Header': 'My-Value'\n    }\n  };\n  return response;\n}\n  \"\"\";\n  late CodeController script;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  String getScriptCode() {\n    return script.text;\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    script = CodeController(language: javascript, text: widget.script ?? template);\n  }\n\n  @override\n  void dispose() {\n    script.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n        height: 320,\n        child: CodeTheme(\n            data: CodeThemeData(styles: monokaiSublimeTheme),\n            child:\n                SingleChildScrollView(child: CodeField(textStyle: const TextStyle(fontSize: 13), controller: script))));\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/request_map.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_map_manager.dart';\nimport 'package:proxypin/ui/component/app_dialog.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/desktop/setting/request_map/map_local.dart';\nimport 'package:proxypin/ui/desktop/setting/request_map/map_scipt.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nimport '../../../../network/util/logger.dart';\n\nbool _refresh = false;\n\n/// 刷新配置\nFuture<void> _refreshConfig({bool force = false}) async {\n  if (force) {\n    _refresh = false;\n    await RequestMapManager.instance.then((manager) => manager.flushConfig());\n    await DesktopMultiWindow.invokeMethod(0, \"refreshRequestMap\");\n    return;\n  }\n\n  if (_refresh) {\n    return;\n  }\n  _refresh = true;\n  Future.delayed(const Duration(milliseconds: 1000), () async {\n    _refresh = false;\n    await RequestMapManager.instance.then((manager) => manager.flushConfig());\n    await DesktopMultiWindow.invokeMethod(0, \"refreshRequestMap\");\n  });\n}\n\nclass RequestMapPage extends StatefulWidget {\n  final int? windowId;\n\n  const RequestMapPage({super.key, this.windowId});\n\n  @override\n  State<StatefulWidget> createState() => _RequestMapPageState();\n}\n\nclass _RequestMapPageState extends State<RequestMapPage> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    HardwareKeyboard.instance.addHandler(onKeyEvent);\n  }\n\n  @override\n  void dispose() {\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if (HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.escape) && Navigator.canPop(context)) {\n      Navigator.maybePop(context);\n      return true;\n    }\n\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      if (_refresh) {\n        _refreshConfig(force: true).whenComplete(() => WindowController.fromWindowId(widget.windowId!).close());\n        return true;\n      }\n      WindowController.fromWindowId(widget.windowId!).close();\n      return true;\n    }\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n            title: Text(localizations.requestMap, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n            toolbarHeight: 36,\n            centerTitle: true),\n        body: Padding(\n            padding: const EdgeInsets.only(left: 15, right: 10),\n            child: futureWidget(\n                RequestMapManager.instance,\n                loading: true,\n                (data) => Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        mainAxisAlignment: MainAxisAlignment.start,\n                        children: [\n                          Row(children: [\n                            SizedBox(\n                                width: 350,\n                                child: ListTile(\n                                    title: Text(\"${localizations.enable} ${localizations.requestMap}\"),\n                                    subtitle:\n                                        Text(localizations.requestMapDescribe, style: const TextStyle(fontSize: 12)),\n                                    trailing: SwitchWidget(\n                                        value: data.enabled,\n                                        scale: 0.8,\n                                        onChanged: (value) {\n                                          data.enabled = value;\n                                          _refreshConfig();\n                                        }))),\n                            Expanded(\n                                child: Row(\n                              mainAxisAlignment: MainAxisAlignment.end,\n                              children: [\n                                const SizedBox(width: 10),\n                                TextButton.icon(\n                                    icon: const Icon(Icons.add, size: 18),\n                                    onPressed: showEdit,\n                                    label: Text(localizations.add)),\n                                const SizedBox(width: 10),\n                                TextButton.icon(\n                                  icon: const Icon(Icons.input_rounded, size: 18),\n                                  onPressed: import,\n                                  label: Text(localizations.import),\n                                ),\n                                const SizedBox(width: 10),\n                              ],\n                            )),\n                            const SizedBox(width: 15)\n                          ]),\n                          const SizedBox(height: 5),\n                          RequestMapList(list: data.rules, windowId: widget.windowId),\n                        ]))));\n  }\n\n  //导入js\n  Future<void> import() async {\n    String? path;\n    if (Platform.isMacOS) {\n      path = await DesktopMultiWindow.invokeMethod(0, \"pickFiles\", {\n        \"allowedExtensions\": ['json']\n      });\n      WindowController.fromWindowId(widget.windowId!).show();\n    } else {\n      FilePickerResult? result =\n          await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']);\n      path = result?.files.single.path;\n    }\n\n    if (path == null) {\n      return;\n    }\n    try {\n      var json = jsonDecode(await File(path).readAsString());\n      var manager = (await RequestMapManager.instance);\n      if (json is List<dynamic>) {\n        for (var item in json) {\n          var mapRule = RequestMapRule.fromJson(item);\n          var requestMapItem = RequestMapItem.fromJson(item['item']);\n          await manager.addRule(mapRule, requestMapItem);\n        }\n      }\n\n      if (mounted) {\n        CustomToast.success(localizations.importSuccess).show(context);\n      }\n      setState(() {});\n    } catch (e, t) {\n      logger.e('[RequestMap] import fail $path', error: e, stackTrace: t);\n      if (mounted) {\n        CustomToast.error(\"${localizations.importFailed} $e\").show(context);\n      }\n    }\n  }\n\n  /// 添加脚本\n  Future<void> showEdit() async {\n    showDialog(barrierDismissible: false, context: context, builder: (_) => RequestMapEdit(windowId: widget.windowId))\n        .then((value) {\n      if (value != null) {\n        setState(() {});\n      }\n    });\n  }\n}\n\n/// 脚本列表\nclass RequestMapList extends StatefulWidget {\n  final int? windowId;\n  final List<RequestMapRule> list;\n\n  const RequestMapList({super.key, required this.list, required this.windowId});\n\n  @override\n  State<RequestMapList> createState() => _RequestMapListState();\n}\n\nclass _RequestMapListState extends State<RequestMapList> {\n  Set<int> selected = {};\n  bool isPressed = false;\n  Offset? lastPressPosition;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n        onSecondaryTap: () {\n          if (lastPressPosition == null) {\n            return;\n          }\n          showGlobalMenu(lastPressPosition!);\n        },\n        onTapDown: (details) {\n          if (selected.isEmpty) {\n            return;\n          }\n          if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n            return;\n          }\n          setState(() {\n            selected.clear();\n          });\n        },\n        child: Listener(\n            onPointerUp: (event) => isPressed = false,\n            onPointerDown: (event) {\n              lastPressPosition = event.localPosition;\n              if (event.buttons == kPrimaryMouseButton) {\n                isPressed = true;\n              }\n            },\n            child: Container(\n                padding: const EdgeInsets.only(top: 10),\n                height: 530,\n                decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n                child: SingleChildScrollView(\n                    child: Column(children: [\n                  Row(\n                    mainAxisAlignment: MainAxisAlignment.start,\n                    children: [\n                      Container(width: 130, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)),\n                      SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)),\n                      const VerticalDivider(),\n                      const Expanded(child: Text(\"URL\")),\n                      SizedBox(width: 100, child: Text(localizations.action, textAlign: TextAlign.center)),\n                    ],\n                  ),\n                  const Divider(thickness: 0.5),\n                  Column(children: rows(widget.list))\n                ])))));\n  }\n\n  List<Widget> rows(List<RequestMapRule> list) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n    bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en');\n\n    return List.generate(list.length, (index) {\n      return InkWell(\n          highlightColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          hoverColor: primaryColor.withOpacity(0.3),\n          onSecondaryTapDown: (details) => showMenus(details, index),\n          onDoubleTap: () => showEdit(index),\n          onHover: (hover) {\n            if (isPressed && !selected.contains(index)) {\n              setState(() {\n                selected.add(index);\n              });\n            }\n          },\n          onTap: () {\n            if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n              setState(() {\n                selected.contains(index) ? selected.remove(index) : selected.add(index);\n              });\n              return;\n            }\n            if (selected.isEmpty) {\n              return;\n            }\n            setState(() {\n              selected.clear();\n            });\n          },\n          child: Container(\n              color: selected.contains(index)\n                  ? primaryColor.withOpacity(0.6)\n                  : index.isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 30,\n              padding: const EdgeInsets.all(5),\n              child: Row(\n                children: [\n                  SizedBox(width: 130, child: Text(list[index].name ?? '', style: const TextStyle(fontSize: 13))),\n                  SizedBox(\n                      width: 40,\n                      child: Transform.scale(\n                          scale: 0.6,\n                          child: SwitchWidget(\n                              value: list[index].enabled,\n                              onChanged: (val) {\n                                list[index].enabled = val;\n                                _refreshConfig();\n                              }))),\n                  const SizedBox(width: 20),\n                  Expanded(\n                      child:\n                          Text(list[index].url, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13))),\n                  SizedBox(\n                      width: 100,\n                      child: Text(isEN ? list[index].type.name.camelCaseToSpaced() : list[index].type.label,\n                          textAlign: TextAlign.center, style: const TextStyle(fontSize: 13))),\n                ],\n              )));\n    });\n  }\n\n  void showGlobalMenu(Offset offset) {\n    showContextMenu(context, offset, items: [\n      PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()),\n      PopupMenuItem(height: 35, child: Text(localizations.export), onTap: () => export(selected.toList())),\n      const PopupMenuDivider(),\n      PopupMenuItem(height: 35, child: Text(localizations.enableSelect), onTap: () => enableStatus(true)),\n      PopupMenuItem(height: 35, child: Text(localizations.disableSelect), onTap: () => enableStatus(false)),\n      const PopupMenuDivider(),\n      PopupMenuItem(height: 35, child: Text(localizations.deleteSelect), onTap: () => remove(selected.toList())),\n    ]);\n  }\n\n  //点击菜单\n  void showMenus(TapDownDetails details, int index) {\n    if (selected.length > 1) {\n      showGlobalMenu(details.globalPosition);\n      return;\n    }\n    setState(() {\n      selected.add(index);\n    });\n\n    showContextMenu(context, details.globalPosition, items: [\n      PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)),\n      PopupMenuItem(height: 35, child: Text(localizations.export), onTap: () => export([index])),\n      PopupMenuItem(\n          height: 35,\n          child: widget.list[index].enabled ? Text(localizations.disabled) : Text(localizations.enable),\n          onTap: () {\n            widget.list[index].enabled = !widget.list[index].enabled;\n            _refreshConfig();\n          }),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35,\n          child: Text(localizations.delete),\n          onTap: () async {\n            var manager = await RequestMapManager.instance;\n            await manager.deleteRule(index);\n            _refreshConfig();\n          }),\n    ]).then((value) {\n      if (mounted) {\n        setState(() {\n          selected.remove(index);\n        });\n      }\n    });\n  }\n\n  Future<void> showEdit([int? index]) async {\n    final item = index == null ? null : await (await RequestMapManager.instance).getMapItem(widget.list[index]);\n    if (!mounted) {\n      return;\n    }\n\n    showDialog(\n            barrierDismissible: false,\n            context: context,\n            builder: (_) =>\n                RequestMapEdit(windowId: widget.windowId, rule: index == null ? null : widget.list[index], item: item))\n        .then((value) {\n      if (value != null) {\n        setState(() {});\n      }\n    });\n  }\n\n  //导出\n  Future<void> export(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    //文件名称\n    String fileName = 'request_map.json';\n    String? path;\n    if (Platform.isMacOS) {\n      path = await DesktopMultiWindow.invokeMethod(0, \"saveFile\", {\"fileName\": fileName});\n\n      if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show();\n    } else {\n      path = await FilePicker.platform.saveFile(fileName: fileName);\n    }\n    if (path == null) {\n      return;\n    }\n\n    var manager = await RequestMapManager.instance;\n    List<dynamic> json = [];\n    for (var idx in indexes) {\n      var item = widget.list[idx];\n      var map = item.toJson();\n      map.remove(\"itemPath\");\n      map['item'] = (await manager.getMapItem(item))?.toJson();\n      json.add(map);\n    }\n\n    await File(path).writeAsBytes(utf8.encode(jsonEncode(json)));\n\n    if (mounted) FlutterToastr.show(localizations.exportSuccess, context);\n  }\n\n  void enableStatus(bool enable) {\n    for (var idx in selected) {\n      widget.list[idx].enabled = enable;\n    }\n    setState(() {});\n    _refreshConfig();\n  }\n\n  Future<void> remove(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    showConfirmDialog(context, content: localizations.confirmContent, onConfirm: () async {\n      var manager = await RequestMapManager.instance;\n      for (var idx in indexes) {\n        await manager.deleteRule(idx);\n      }\n\n      setState(() {\n        selected.clear();\n      });\n      _refreshConfig(force: true);\n\n      if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n    });\n  }\n}\n\n///请求重写规则添加对话框\nclass RequestMapEdit extends StatefulWidget {\n  final RequestMapRule? rule;\n  final RequestMapItem? item;\n  final int? windowId;\n  final String? url;\n  final String? title;\n\n  const RequestMapEdit({super.key, this.rule, this.windowId, this.item, this.url, this.title});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _RequestMapEditState();\n  }\n}\n\nclass _RequestMapEditState extends State<RequestMapEdit> {\n  final mapLocalKey = GlobalKey<MapLocaleState>();\n  final mapScriptKey = GlobalKey<MapScriptState>();\n\n  late RequestMapRule rule;\n\n  late RequestMapType mapType;\n  late TextEditingController nameInput;\n  late TextEditingController urlInput;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    rule = widget.rule ?? RequestMapRule(url: widget.url ?? '', type: RequestMapType.local);\n    mapType = rule.type;\n    nameInput = TextEditingController(text: rule.name ?? widget.title);\n    urlInput = TextEditingController(text: rule.url);\n  }\n\n  @override\n  void dispose() {\n    urlInput.dispose();\n    nameInput.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    GlobalKey formKey = GlobalKey<FormState>();\n    bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en');\n\n    return AlertDialog(\n        scrollable: true,\n        titlePadding: const EdgeInsets.only(top: 10, left: 20),\n        actionsPadding: const EdgeInsets.only(right: 15, bottom: 15),\n        contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),\n        title: Row(children: [\n          Text(localizations.requestRewriteRule, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n        ]),\n        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),\n        content: Container(\n            width: 550,\n            constraints: const BoxConstraints(minHeight: 200, maxHeight: 530),\n            child: Form(\n                key: formKey,\n                child: Column(\n                    mainAxisAlignment: MainAxisAlignment.start,\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: <Widget>[\n                      Row(children: [\n                        SizedBox(width: 55, child: Text('${localizations.enable}:')),\n                        SwitchWidget(value: rule.enabled, onChanged: (val) => rule.enabled = val, scale: 0.8)\n                      ]),\n                      const SizedBox(height: 5),\n                      textField('${localizations.name}:', nameInput, localizations.pleaseEnter),\n                      const SizedBox(height: 5),\n                      textField('URL:', urlInput, 'https://www.example.com/api/*', required: true),\n                      const SizedBox(height: 5),\n                      Row(children: [\n                        SizedBox(width: 60, child: Text('${localizations.action}:')),\n                        SizedBox(\n                            width: 150,\n                            height: 33,\n                            child: DropdownButtonFormField<RequestMapType>(\n                              onSaved: (val) => rule.type = val!,\n                              value: mapType,\n                              decoration: InputDecoration(\n                                  errorStyle: const TextStyle(height: 0, fontSize: 0),\n                                  contentPadding: const EdgeInsets.only(left: 7, right: 7),\n                                  focusedBorder: focusedBorder(),\n                                  border: const OutlineInputBorder()),\n                              items: RequestMapType.values\n                                  .map((e) => DropdownMenuItem(\n                                      value: e,\n                                      child: Text(isEN ? e.name : e.label, style: const TextStyle(fontSize: 13))))\n                                  .toList(),\n                              onChanged: onChangeType,\n                            )),\n                        const SizedBox(width: 10),\n                      ]),\n                      const SizedBox(height: 10),\n                      mapRule(),\n                    ]))),\n        actions: [\n          ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop()),\n          FilledButton(\n              child: Text(localizations.save),\n              onPressed: () async {\n                if (!(formKey.currentState as FormState).validate()) {\n                  FlutterToastr.show(localizations.cannotBeEmpty, context, position: FlutterToastr.center);\n                  return;\n                }\n\n                (formKey.currentState as FormState).save();\n                rule.name = nameInput.text;\n                rule.url = urlInput.text;\n                rule.type = mapType;\n                RequestMapItem item;\n                if (mapType == RequestMapType.local) {\n                  item = mapLocalKey.currentState!.getRequestMapItem();\n                } else {\n                  String? scriptCode = mapScriptKey.currentState?.getScriptCode();\n                  item = widget.item ?? RequestMapItem();\n                  item.script = scriptCode;\n                }\n\n                var requestMapManager = await RequestMapManager.instance;\n                var index = requestMapManager.rules.indexOf(rule);\n                if (index >= 0) {\n                  await requestMapManager.updateRule(rule, item);\n                } else {\n                  await requestMapManager.addRule(rule, item);\n                }\n\n                DesktopMultiWindow.invokeMethod(0, \"refreshRequestMap\");\n                if (mounted) {\n                  Navigator.of(this.context).pop(rule);\n                }\n              })\n        ]);\n  }\n\n  void onChangeType(RequestMapType? val) async {\n    if (mapType == val) return;\n    mapType = val!;\n    setState(() {});\n  }\n\n  Widget mapRule() {\n    if (mapType == RequestMapType.script) {\n      return DesktopMapScript(key: mapScriptKey, script: widget.item?.script);\n    }\n\n    return DesktopMapLocal(key: mapLocalKey, item: widget.item, windowId: widget.windowId);\n  }\n\n  Widget textField(String label, TextEditingController controller, String hint,\n      {bool required = false, FormFieldSetter<String>? onSaved}) {\n    return Row(children: [\n      SizedBox(width: 60, child: Text(label)),\n      Expanded(\n          child: TextFormField(\n        controller: controller,\n        style: const TextStyle(fontSize: 14),\n        validator: (val) => val?.isNotEmpty == true || !required ? null : \"\",\n        onSaved: onSaved,\n        decoration: InputDecoration(\n            hintText: hint,\n            constraints: const BoxConstraints(minHeight: 38),\n            hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n            contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),\n            errorStyle: const TextStyle(height: 0, fontSize: 0),\n            focusedBorder: focusedBorder(),\n            isDense: true,\n            border: const OutlineInputBorder()),\n      ))\n    ]);\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/request_rewrite.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/multi_window.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/desktop/setting/rewrite/rewrite_replace.dart';\nimport 'package:proxypin/ui/desktop/setting/rewrite/rewrite_update.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nimport '../../component/http_method_popup.dart';\n\n/// @author wanghongen\n/// 2023/10/8\nclass RequestRewriteWidget extends StatefulWidget {\n  final int windowId;\n  final RequestRewriteManager requestRewrites;\n\n  const RequestRewriteWidget({super.key, required this.windowId, required this.requestRewrites});\n\n  @override\n  State<StatefulWidget> createState() {\n    return RequestRewriteState();\n  }\n}\n\nclass RequestRewriteState extends State<RequestRewriteWidget> {\n  late ValueNotifier<bool> enableNotifier;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    HardwareKeyboard.instance.addHandler(onKeyEvent);\n    enableNotifier = ValueNotifier(widget.requestRewrites.enabled == true);\n  }\n\n  @override\n  void dispose() {\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if (HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.escape) && Navigator.canPop(context)) {\n      Navigator.maybePop(context);\n      return true;\n    }\n\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      if (Navigator.canPop(context)) {\n        Navigator.pop(context);\n        return true;\n      }\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      WindowController.fromWindowId(widget.windowId).close();\n      return true;\n    }\n\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        backgroundColor: Theme.of(context).dialogBackgroundColor,\n        appBar: AppBar(\n            title:\n                Text(localizations.requestRewrite, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n            toolbarHeight: 34,\n            centerTitle: true),\n        body: Padding(\n            padding: const EdgeInsets.only(left: 15, right: 10),\n            child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n              Row(children: [\n                SizedBox(\n                    width: 280,\n                    child: ValueListenableBuilder(\n                        valueListenable: enableNotifier,\n                        builder: (_, bool v, __) {\n                          return Transform.scale(\n                              scale: 0.8,\n                              child: SwitchListTile(\n                                  contentPadding: const EdgeInsets.only(left: 2),\n                                  title: Text(localizations.requestRewriteEnable),\n                                  value: enableNotifier.value,\n                                  onChanged: (value) {\n                                    enableNotifier.value = value;\n                                    MultiWindow.invokeRefreshRewrite(Operation.enabled, enabled: value);\n                                  }));\n                        })),\n                const SizedBox(width: 10),\n                Expanded(\n                    child: Row(\n                  mainAxisAlignment: MainAxisAlignment.end,\n                  children: [\n                    IconButton(\n                        onPressed: refresh,\n                        icon: const Icon(Icons.refresh, color: Colors.blue),\n                        tooltip: localizations.refresh),\n                    const SizedBox(width: 10),\n                    TextButton.icon(\n                      icon: const Icon(Icons.add, size: 18),\n                      label: Text(localizations.add),\n                      onPressed: add,\n                    ),\n                    const SizedBox(width: 5),\n                    TextButton.icon(\n                      icon: const Icon(Icons.input_rounded, size: 18),\n                      onPressed: import,\n                      label: Text(localizations.import),\n                    )\n                  ],\n                )),\n                const SizedBox(width: 15)\n              ]),\n              const SizedBox(height: 10),\n              RequestRuleList(widget.requestRewrites, windowId: widget.windowId),\n            ])));\n  }\n\n  //刷新\n  void refresh() async {\n    await widget.requestRewrites.reloadRequestRewrite();\n    setState(() {});\n  }\n\n  //导入js\n  Future<void> import() async {\n    String? path;\n    if (Platform.isMacOS) {\n      path = await DesktopMultiWindow.invokeMethod(0, \"pickFiles\", {\n        \"allowedExtensions\": ['config', 'json']\n      });\n      WindowController.fromWindowId(widget.windowId).show();\n    } else {\n      FilePickerResult? result =\n          await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['config', 'json']);\n      path = result?.files.single.path;\n    }\n\n    if (path == null) {\n      return;\n    }\n\n    try {\n      List json = jsonDecode(await File(path).readAsString());\n      for (var item in json) {\n        var rule = RequestRewriteRule.formJson(item);\n        var items = (item['items'] as List).map((e) => RewriteItem.fromJson(e)).toList();\n\n        widget.requestRewrites.addRule(rule, items);\n        await MultiWindow.invokeRefreshRewrite(Operation.add, rule: rule, items: items);\n      }\n\n      if (mounted) {\n        FlutterToastr.show(localizations.importSuccess, context);\n      }\n      setState(() {});\n    } catch (e, t) {\n      logger.e('导入失败 $path', error: e, stackTrace: t);\n      if (mounted) {\n        FlutterToastr.show(\"${localizations.importFailed} $e\", context);\n      }\n    }\n  }\n\n  void add() {\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (BuildContext context) => RewriteRuleEdit(windowId: widget.windowId)).then((value) {\n      if (value != null) setState(() {});\n    });\n  }\n}\n\n///请求重写规则列表\nclass RequestRuleList extends StatefulWidget {\n  final int windowId;\n  final RequestRewriteManager requestRewrites;\n\n  const RequestRuleList(this.requestRewrites, {super.key, required this.windowId});\n\n  @override\n  State<RequestRuleList> createState() => _RequestRuleListState();\n}\n\nclass _RequestRuleListState extends State<RequestRuleList> {\n  Map<int, bool> selected = {};\n  late List<RequestRewriteRule> rules;\n  bool isPressed = false;\n  Offset? lastPressPosition;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  initState() {\n    super.initState();\n    rules = widget.requestRewrites.rules;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n        onSecondaryTap: () {\n          if (lastPressPosition == null) {\n            return;\n          }\n          showGlobalMenu(lastPressPosition!);\n        },\n        onTapDown: (details) {\n          if (selected.isEmpty) {\n            return;\n          }\n          if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n            return;\n          }\n          setState(() {\n            selected.clear();\n          });\n        },\n        child: Listener(\n            onPointerUp: (event) => isPressed = false,\n            onPointerDown: (event) {\n              lastPressPosition = event.localPosition;\n              if (event.buttons == kPrimaryMouseButton) {\n                isPressed = true;\n              }\n            },\n            child: Container(\n                padding: const EdgeInsets.only(top: 10),\n                constraints: const BoxConstraints(maxHeight: 600, minHeight: 550),\n                decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n                child: SingleChildScrollView(\n                    child: Column(children: [\n                  Row(\n                    mainAxisAlignment: MainAxisAlignment.start,\n                    children: [\n                      Container(width: 130, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)),\n                      SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)),\n                      const VerticalDivider(),\n                      const Expanded(child: Text(\"URL\")),\n                      SizedBox(width: 100, child: Text(localizations.action, textAlign: TextAlign.center)),\n                    ],\n                  ),\n                  const Divider(thickness: 0.5),\n                  Column(children: rows(widget.requestRewrites.rules))\n                ])))));\n  }\n\n  void enableStatus(bool enable) {\n    if (selected.isEmpty) return;\n    selected.forEach((key, value) {\n      if (rules[key].enabled == enable) return;\n\n      rules[key].enabled = enable;\n      MultiWindow.invokeRefreshRewrite(Operation.update, index: key, rule: rules[key]);\n    });\n\n    setState(() {});\n  }\n\n  void showGlobalMenu(Offset offset) {\n    showContextMenu(context, offset, items: [\n      PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()),\n      PopupMenuItem(height: 35, child: Text(localizations.export), onTap: () => export(selected.keys.toList())),\n      const PopupMenuDivider(),\n      PopupMenuItem(height: 35, child: Text(localizations.enableSelect), onTap: () => enableStatus(true)),\n      PopupMenuItem(height: 35, child: Text(localizations.disableSelect), onTap: () => enableStatus(false)),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35, child: Text(localizations.deleteSelect), onTap: () => removeRewrite(selected.keys.toList())),\n    ]);\n  }\n\n  List<Widget> rows(List<RequestRewriteRule> list) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n    bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en');\n\n    return List.generate(list.length, (index) {\n      return InkWell(\n          highlightColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          hoverColor: primaryColor.withOpacity(0.3),\n          onSecondaryTapDown: (details) => showMenus(details, index),\n          onDoubleTap: () => showEdit(index),\n          onHover: (hover) {\n            if (isPressed && selected[index] != true) {\n              setState(() {\n                selected[index] = true;\n              });\n            }\n          },\n          onTap: () {\n            if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n              setState(() {\n                selected[index] = !(selected[index] ?? false);\n              });\n              return;\n            }\n            if (selected.isEmpty) {\n              return;\n            }\n            setState(() {\n              selected.clear();\n            });\n          },\n          child: Container(\n              color: selected[index] == true\n                  ? primaryColor.withOpacity(0.6)\n                  : index.isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 30,\n              padding: const EdgeInsets.all(5),\n              child: Row(\n                children: [\n                  SizedBox(width: 130, child: Text(list[index].name ?? '', style: const TextStyle(fontSize: 13))),\n                  SizedBox(\n                      width: 40,\n                      child: SwitchWidget(\n                          scale: 0.6,\n                          value: list[index].enabled,\n                          onChanged: (val) {\n                            list[index].enabled = val;\n                            MultiWindow.invokeRefreshRewrite(Operation.update, index: index, rule: list[index]);\n                          })),\n                  const SizedBox(width: 20),\n                  Expanded(\n                      child:\n                          Text(list[index].url, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13))),\n                  SizedBox(\n                      width: 100,\n                      child: Text(isEN ? list[index].type.name.camelCaseToSpaced() : list[index].type.label,\n                          textAlign: TextAlign.center, style: const TextStyle(fontSize: 13))),\n                ],\n              )));\n    });\n  }\n\n  //导出\n  Future<void> export(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n\n    String fileName = 'proxypin-rewrites.config';\n\n    String? path;\n    if (Platform.isMacOS) {\n      path = await DesktopMultiWindow.invokeMethod(0, \"saveFile\", {\"fileName\": fileName});\n      WindowController.fromWindowId(widget.windowId).show();\n    } else {\n      path = await FilePicker.platform.saveFile(fileName: fileName);\n    }\n\n    if (path == null) {\n      return;\n    }\n\n    var list = [];\n    for (var index in indexes) {\n      var rule = widget.requestRewrites.rules[index];\n      var json = rule.toJson();\n      json.remove(\"rewritePath\");\n      json['items'] = await widget.requestRewrites.getRewriteItems(rule);\n      list.add(json);\n    }\n\n    await File(path).writeAsBytes(utf8.encode(jsonEncode(list)));\n    if (mounted) FlutterToastr.show(localizations.exportSuccess, context);\n  }\n\n  //删除\n  Future<void> removeRewrite(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    return showConfirmDialog(context, content: localizations.requestRewriteDeleteConfirm(indexes.length),\n        onConfirm: () async {\n      var list = indexes.toList();\n      list.sort((a, b) => b.compareTo(a));\n      for (var value in list) {\n        await widget.requestRewrites.removeIndex([value]);\n        MultiWindow.invokeRefreshRewrite(Operation.delete, index: value);\n      }\n\n      setState(() {\n        selected.clear();\n      });\n      if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n    });\n  }\n\n  Future<void> showEdit([int? index]) async {\n    RequestRewriteRule? rule;\n    List<RewriteItem>? rewriteItems;\n\n    if (index != null) {\n      rule = widget.requestRewrites.rules[index];\n      rewriteItems = await widget.requestRewrites.getRewriteItems(rule);\n    }\n    if (!mounted) return;\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (BuildContext context) {\n          return RewriteRuleEdit(rule: rule, items: rewriteItems, windowId: widget.windowId);\n        }).then((value) {\n      if (value != null) {\n        setState(() {});\n      }\n    });\n  }\n\n  //点击菜单\n  void showMenus(TapDownDetails details, int index) {\n    if (selected.length > 1) {\n      showGlobalMenu(details.globalPosition);\n      return;\n    }\n    setState(() {\n      selected[index] = true;\n    });\n    showContextMenu(context, details.globalPosition, items: [\n      PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)),\n      PopupMenuItem(height: 35, onTap: () => export([index]), child: Text(localizations.export)),\n      PopupMenuItem(\n          height: 35,\n          child: rules[index].enabled ? Text(localizations.disabled) : Text(localizations.enable),\n          onTap: () {\n            rules[index].enabled = !rules[index].enabled;\n            MultiWindow.invokeRefreshRewrite(Operation.update, index: index, rule: rules[index]);\n          }),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35,\n          child: Text(localizations.delete),\n          onTap: () async {\n            await widget.requestRewrites.removeIndex([index]);\n            MultiWindow.invokeRefreshRewrite(Operation.delete, index: index);\n            // setState(() {});\n          })\n    ]).then((value) {\n      setState(() {\n        selected.remove(index);\n      });\n    });\n  }\n}\n\n///请求重写规则添加对话框\nclass RewriteRuleEdit extends StatefulWidget {\n  final RequestRewriteRule? rule;\n  final List<RewriteItem>? items;\n  final HttpRequest? request;\n  final int? windowId;\n\n  const RewriteRuleEdit({super.key, this.rule, this.items, this.windowId, this.request});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _RewriteRuleEditState();\n  }\n}\n\nclass _RewriteRuleEditState extends State<RewriteRuleEdit> {\n  final rewriteReplaceKey = GlobalKey<RewriteReplaceState>();\n  final rewriteUpdateKey = GlobalKey<RewriteUpdateState>();\n\n  late RequestRewriteRule rule;\n  List<RewriteItem>? items;\n\n  late RuleType ruleType;\n  late TextEditingController nameInput;\n  late TextEditingController urlInput;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    rule = widget.rule ?? RequestRewriteRule(url: '', type: RuleType.responseReplace);\n    items = widget.items;\n    ruleType = rule.type;\n    nameInput = TextEditingController(text: rule.name);\n    urlInput = TextEditingController(text: rule.url);\n\n    if (items == null && widget.request != null) {\n      items = fromRequestItems(widget.request!, ruleType);\n    }\n  }\n\n  @override\n  void dispose() {\n    urlInput.dispose();\n    nameInput.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    GlobalKey formKey = GlobalKey<FormState>();\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return AlertDialog(\n        scrollable: true,\n        titlePadding: const EdgeInsets.only(top: 10, left: 20),\n        actionsPadding: const EdgeInsets.only(right: 15, bottom: 15),\n        contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),\n        title: Row(children: [\n          Text(localizations.requestRewriteRule, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n          const SizedBox(width: 20),\n          Text.rich(TextSpan(\n              text: localizations.useGuide,\n              style: const TextStyle(color: Colors.blue, fontSize: 14),\n              recognizer: TapGestureRecognizer()\n                ..onTap = () => DesktopMultiWindow.invokeMethod(\n                    0,\n                    \"launchUrl\",\n                    isCN\n                        ? 'https://gitee.com/wanghongenpin/proxypin/wikis/%E8%AF%B7%E6%B1%82%E9%87%8D%E5%86%99'\n                        : 'https://github.com/wanghongenpin/proxypin/wiki/Request-Rewrite'))),\n        ]),\n        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),\n        content: Container(\n            width: 550,\n            constraints: const BoxConstraints(minHeight: 200, maxHeight: 560),\n            child: Form(\n                key: formKey,\n                child: Column(\n                    mainAxisAlignment: MainAxisAlignment.start,\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: <Widget>[\n                      Row(children: [\n                        SizedBox(width: 55, child: Text('${localizations.enable}:')),\n                        SwitchWidget(value: rule.enabled, onChanged: (val) => rule.enabled = val, scale: 0.8)\n                      ]),\n                      const SizedBox(height: 5),\n                      textField('${localizations.name}:', nameInput, localizations.pleaseEnter),\n                      const SizedBox(height: 10),\n                      // URL input with Method as prefix (method shown before the URL field)\n                      Row(children: [\n                        SizedBox(width: 60, child: Text('URL:')),\n                        Expanded(\n                          child: TextFormField(\n                            controller: urlInput,\n                            style: const TextStyle(fontSize: 14),\n                            validator: (val) => val?.isNotEmpty == true ? null : \"\",\n                            decoration: InputDecoration(\n                              hintText: 'https://www.example.com/api/*',\n                              hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n                              contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10),\n                              errorStyle: const TextStyle(height: 0, fontSize: 0),\n                              focusedBorder: focusedBorder(),\n                              isDense: true,\n                              border: const OutlineInputBorder(),\n                              prefixIcon: Padding(\n                                padding: const EdgeInsets.only(left: 6, right: 6),\n                                child: MethodPopupMenu(\n                                  value: rule.method,\n                                  showSeparator: true,\n                                  onChanged: (val) {\n                                    setState(() {\n                                      rule.method = val;\n                                    });\n                                  },\n                                ),\n                              ),\n                            ),\n                          ),\n                        ),\n                      ]),\n                      const SizedBox(height: 10),\n                      Row(children: [\n                        SizedBox(width: 60, child: Text('${localizations.action}:')),\n                        SizedBox(\n                            width: 150,\n                            height: 36,\n                            child: DropdownButtonFormField<RuleType>(\n                              onSaved: (val) => rule.type = val!,\n                              value: ruleType,\n                              decoration: InputDecoration(\n                                  errorStyle: const TextStyle(height: 0, fontSize: 0),\n                                  contentPadding: const EdgeInsets.only(left: 7, right: 7),\n                                  focusedBorder: focusedBorder(),\n                                  border: const OutlineInputBorder()),\n                              items: RuleType.values\n                                  .map((e) => DropdownMenuItem(\n                                      value: e,\n                                      child: Text(isCN ? e.label : e.name, style: const TextStyle(fontSize: 14))))\n                                  .toList(),\n                              onChanged: onChangeType,\n                            )),\n                        const SizedBox(width: 10),\n                      ]),\n                      const SizedBox(height: 10),\n                      rewriteRule(),\n                    ]))),\n        actions: [\n          ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop()),\n          FilledButton(\n              child: Text(localizations.save),\n              onPressed: () async {\n                if (!(formKey.currentState as FormState).validate()) {\n                  FlutterToastr.show(localizations.cannotBeEmpty, context, position: FlutterToastr.center);\n                  return;\n                }\n\n                (formKey.currentState as FormState).save();\n                rule.name = nameInput.text;\n                rule.url = urlInput.text;\n                // method already set on change\n                items = rewriteReplaceKey.currentState?.getItems() ?? rewriteUpdateKey.currentState?.getItems();\n\n                var requestRewrites = await RequestRewriteManager.instance;\n                requestRewrites.rewriteItemsCache[rule] = items!;\n                var index = requestRewrites.rules.indexOf(rule);\n\n                if (index >= 0) {\n                  //存在 更新重写\n                  MultiWindow.invokeRefreshRewrite(Operation.update, index: index, rule: rule, items: items);\n                } else {\n                  //添加\n                  if (widget.windowId != null) {\n                    requestRewrites.rules.add(rule);\n                  }\n\n                  MultiWindow.invokeRefreshRewrite(Operation.add, rule: rule, items: items);\n                }\n                if (mounted) {\n                  Navigator.of(this.context).pop(rule);\n                }\n              })\n        ]);\n  }\n\n  void onChangeType(RuleType? val) async {\n    if (ruleType == val) return;\n\n    ruleType = val!;\n    items = [];\n\n    if (ruleType == widget.rule?.type) {\n      items = widget.items;\n    } else if (widget.request != null) {\n      items?.addAll(fromRequestItems(widget.request!, ruleType));\n    }\n\n    setState(() {\n      rewriteReplaceKey.currentState?.initItems(ruleType, items);\n      rewriteUpdateKey.currentState?.initItems(ruleType, items);\n    });\n  }\n\n  static List<RewriteItem> fromRequestItems(HttpRequest request, RuleType ruleType) {\n    if (ruleType == RuleType.requestReplace) {\n      //请求替换\n      return RewriteItem.fromRequest(request);\n    } else if (ruleType == RuleType.responseReplace && request.response != null) {\n      //响应替换\n      return RewriteItem.fromResponse(request.response!);\n    }\n    return [];\n  }\n\n  Widget rewriteRule() {\n    if (ruleType == RuleType.requestUpdate || ruleType == RuleType.responseUpdate) {\n      return DesktopRewriteUpdate(key: rewriteUpdateKey, items: items, ruleType: ruleType, request: widget.request);\n    }\n\n    return DesktopRewriteReplace(key: rewriteReplaceKey, items: items, ruleType: ruleType, windowId: widget.windowId);\n  }\n\n  Widget textField(String label, TextEditingController controller, String hint,\n      {bool required = false, FormFieldSetter<String>? onSaved}) {\n    return Row(children: [\n      SizedBox(width: 60, child: Text(label)),\n      Expanded(\n          child: TextFormField(\n        controller: controller,\n        style: const TextStyle(fontSize: 14),\n        validator: (val) => val?.isNotEmpty == true || !required ? null : \"\",\n        onSaved: onSaved,\n        decoration: InputDecoration(\n            hintText: hint,\n            hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n            contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),\n            errorStyle: const TextStyle(height: 0, fontSize: 0),\n            focusedBorder: focusedBorder(),\n            isDense: true,\n            border: const OutlineInputBorder()),\n      ))\n    ]);\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/rewrite/rewrite_replace.dart",
    "content": "﻿/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/ui/component/state_component.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/utils/lang.dart';\n\n/// 重写替换\n/// @author wanghongen\n/// 2023/10/8\nclass DesktopRewriteReplace extends StatefulWidget {\n  final int? windowId;\n  final RuleType ruleType;\n  final List<RewriteItem>? items;\n\n  const DesktopRewriteReplace({super.key, this.items, required this.ruleType, this.windowId});\n\n  @override\n  State<DesktopRewriteReplace> createState() => RewriteReplaceState();\n}\n\nclass RewriteReplaceState extends State<DesktopRewriteReplace> {\n  final _headerKey = GlobalKey<HeadersState>();\n  final bodyTextController = TextEditingController();\n  late RuleType ruleType;\n  List<RewriteItem> items = [];\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  initState() {\n    super.initState();\n    ruleType = widget.ruleType;\n    initItems(widget.ruleType, widget.items);\n  }\n\n  @override\n  dispose() {\n    bodyTextController.dispose();\n    super.dispose();\n  }\n\n  ///初始化重写项\n  initItems(RuleType ruleType, List<RewriteItem>? items) {\n    this.items.clear();\n    this.ruleType = ruleType;\n    if (ruleType == RuleType.redirect) {\n      _initRewriteItem(items, RewriteType.redirect, enabled: true);\n      return;\n    }\n\n    if (ruleType == RuleType.requestReplace) {\n      _initRewriteItem(items, RewriteType.replaceRequestLine);\n      _initRewriteItem(items, RewriteType.replaceRequestHeader);\n      _initRewriteItem(items, RewriteType.replaceRequestBody, enabled: true);\n      return;\n    }\n\n    if (ruleType == RuleType.responseReplace) {\n      _initRewriteItem(items, RewriteType.replaceResponseStatus);\n      _initRewriteItem(items, RewriteType.replaceResponseHeader);\n      _initRewriteItem(items, RewriteType.replaceResponseBody, enabled: true);\n      return;\n    }\n  }\n\n  RewriteItem _initRewriteItem(List<RewriteItem>? items, RewriteType type, {bool enabled = false}) {\n    var item = items?.firstWhereOrNull((it) => it.type == type);\n    RewriteItem rewriteItem = RewriteItem(type, item?.enabled ?? enabled, values: item?.values);\n    this.items.add(rewriteItem);\n\n    if (type == RewriteType.replaceRequestHeader || type == RewriteType.replaceResponseHeader) {\n      _headerKey.currentState?.setHeaders(rewriteItem.headers);\n    }\n\n    if ((type == RewriteType.replaceResponseBody || type == RewriteType.replaceRequestBody) &&\n        rewriteItem.bodyType != ReplaceBodyType.file.name) {\n      bodyTextController.text = rewriteItem.body ?? '';\n    }\n\n    return rewriteItem;\n  }\n\n  List<RewriteItem> getItems() {\n    var headers = _headerKey.currentState?.getHeaders();\n    if (headers != null) {\n      items\n          .firstWhere(\n              (item) => item.type == RewriteType.replaceRequestHeader || item.type == RewriteType.replaceResponseHeader)\n          .headers = headers;\n    }\n    return items;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (ruleType == RuleType.redirect) {\n      return SizedBox(\n          height: 120, child: Padding(padding: EdgeInsets.symmetric(vertical: 15), child: redirectEdit(items.first)));\n    }\n\n    if (ruleType == RuleType.responseReplace || ruleType == RuleType.requestReplace) {\n      bool requestEdited = ruleType == RuleType.requestReplace;\n      List<String> tabs = requestEdited\n          ? [localizations.requestLine, localizations.requestHeader, localizations.requestBody]\n          : [localizations.statusCode, localizations.responseHeader, localizations.responseBody];\n\n      return Container(\n        constraints: const BoxConstraints(maxHeight: 370),\n        child: DefaultTabController(\n            length: tabs.length,\n            initialIndex: tabs.length - 1,\n            child: Scaffold(\n              appBar: tabBar(tabs),\n              body: TabBarView(children: [\n                KeepAliveWrapper(child: requestEdited ? requestLine() : statusCodeEdit()),\n                KeepAliveWrapper(child: headers()),\n                KeepAliveWrapper(child: body())\n              ]),\n            )),\n      );\n    }\n\n    return Container();\n  }\n\n  //tabBar\n  TabBar tabBar(List<String> tabs) {\n    return TabBar(\n        tabs: tabs\n            .map((label) => Tab(\n                height: 38,\n                child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [\n                  Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n                  const SizedBox(width: 5),\n                  Dot(color: items[tabs.indexOf(label)].enabled ? const Color(0xFF00FF00) : Colors.grey)\n                ])))\n            .toList());\n  }\n\n  bool jsonFormatted = false;\n\n  //body\n  Widget body() {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    var rewriteItem = items.firstWhere(\n        (item) => item.type == RewriteType.replaceRequestBody || item.type == RewriteType.replaceResponseBody);\n\n    return Column(children: [\n      Row(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [\n        const SizedBox(width: 5),\n        Text(\"${localizations.type}: \"),\n        SizedBox(\n            width: 90,\n            child: DropdownButtonFormField<String>(\n                value: rewriteItem.bodyType ?? ReplaceBodyType.text.name,\n                focusColor: Colors.transparent,\n                itemHeight: 48,\n                decoration:\n                    const InputDecoration(contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none),\n                items: ReplaceBodyType.values\n                    .map((e) => DropdownMenuItem(\n                        value: e.name,\n                        child: Text(isCN ? e.label : e.name.toUpperCase(),\n                            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500))))\n                    .toList(),\n                onChanged: (val) => setState(() {\n                      rewriteItem.bodyType = val!;\n                    }))),\n        Expanded(\n            child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n          IconButton(\n            tooltip: 'JSON Format',\n            icon:\n                Icon(Icons.data_object, size: 20, color: jsonFormatted ? Theme.of(context).colorScheme.primary : null),\n            onPressed: () {\n              setState(() {\n                jsonFormatted = !jsonFormatted;\n                bodyTextController.text =\n                    jsonFormatted ? JSON.pretty(bodyTextController.text) : JSON.compact(bodyTextController.text);\n              });\n            },\n          ),\n          const SizedBox(width: 15),\n          Text(localizations.enable),\n          const SizedBox(width: 10),\n          SwitchWidget(\n              value: rewriteItem.enabled,\n              scale: 0.65,\n              onChanged: (val) => setState(() {\n                    rewriteItem.enabled = val;\n                  }))\n        ]))\n      ]),\n      const SizedBox(height: 10),\n      if (rewriteItem.bodyType == ReplaceBodyType.file.name)\n        fileBodyEdit(rewriteItem)\n      else\n        TextFormField(\n            controller: bodyTextController,\n            style: const TextStyle(fontSize: 14),\n            maxLines: 12,\n            decoration: decoration(localizations.replaceBodyWith,\n                hintText: '${localizations.example} {\"code\":\"200\",\"data\":{}}'),\n            onChanged: (val) => rewriteItem.body = val)\n    ]);\n  }\n\n  Widget fileBodyEdit(RewriteItem item) {\n    //选择文件  删除\n    return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [\n      Expanded(\n          child: item.bodyFile == null\n              ? Container(height: 50)\n              : Container(\n                  padding: const EdgeInsets.all(5),\n                  foregroundDecoration:\n                      BoxDecoration(border: Border.all(color: Theme.of(context).colorScheme.primary, width: 1)),\n                  child: Text(item.bodyFile ?? ''))),\n      const SizedBox(width: 10),\n      FilledButton(\n          onPressed: () async {\n            String? path;\n            if (Platform.isMacOS) {\n              path = await DesktopMultiWindow.invokeMethod(0, \"pickFiles\");\n              if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show();\n            } else {\n              FilePickerResult? result = await FilePicker.platform.pickFiles();\n              path = result?.files.single.path;\n            }\n\n            if (path == null) {\n              return;\n            }\n            item.bodyFile = path;\n            setState(() {});\n          },\n          child: Text(localizations.selectFile, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500))),\n      const SizedBox(width: 10),\n      FilledButton(\n          onPressed: () {\n            setState(() {\n              item.bodyFile = null;\n            });\n          },\n          child: Text(localizations.delete, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500))),\n    ]);\n  }\n\n  //headers\n  Widget headers() {\n    var rewriteItem = items.firstWhere(\n        (item) => item.type == RewriteType.replaceRequestHeader || item.type == RewriteType.replaceResponseHeader);\n\n    return Column(children: [\n      Row(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [\n        const Text('Headers'),\n        const SizedBox(width: 10),\n        Expanded(\n            child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n          Text(localizations.enable),\n          const SizedBox(width: 10),\n          SwitchWidget(\n              value: rewriteItem.enabled,\n              scale: 0.65,\n              onChanged: (val) => setState(() {\n                    rewriteItem.enabled = val;\n                  }))\n        ]))\n      ]),\n      Expanded(child: Headers(headers: rewriteItem.headers, key: _headerKey))\n    ]);\n  }\n\n  ///请求行\n  Widget requestLine() {\n    var rewriteItem = items.firstWhere((item) => item.type == RewriteType.replaceRequestLine);\n    return Column(\n      children: [\n        Row(children: [\n          Text(localizations.requestMethod),\n          const SizedBox(width: 10),\n          SizedBox(\n              width: 120,\n              child: DropdownButtonFormField<String>(\n                  value: rewriteItem.method?.name ?? 'GET',\n                  focusColor: Colors.transparent,\n                  itemHeight: 48,\n                  decoration: const InputDecoration(\n                      contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none),\n                  items: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']\n                      .map((e) => DropdownMenuItem(\n                          value: e, child: Text(e, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))))\n                      .toList(),\n                  onChanged: (val) {\n                    setState(() {\n                      rewriteItem.values['method'] = val!;\n                    });\n                  })),\n          Expanded(\n              child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n            Text(localizations.enable),\n            const SizedBox(width: 10),\n            SwitchWidget(\n                value: rewriteItem.enabled,\n                scale: 0.65,\n                onChanged: (val) {\n                  setState(() {\n                    rewriteItem.enabled = val;\n                  });\n                })\n          ])),\n        ]),\n        const SizedBox(height: 15),\n        textField(\"Path\", rewriteItem.path, \"${localizations.example} /api/v1/user\",\n            onChanged: (val) => rewriteItem.path = val),\n        const SizedBox(height: 15),\n        textField(\"URL${localizations.param}\", rewriteItem.queryParam, \"${localizations.example} id=1&name=2\",\n            onChanged: (val) => rewriteItem.queryParam = val),\n      ],\n    );\n  }\n\n  //重定向\n  Widget redirectEdit(RewriteItem rewriteItem) {\n    return TextFormField(\n        decoration: decoration(localizations.redirectTo, hintText: 'https://www.example.com/api'),\n        maxLines: 5,\n        style: const TextStyle(fontSize: 14),\n        initialValue: rewriteItem.redirectUrl,\n        onChanged: (val) => rewriteItem.redirectUrl = val,\n        validator: (val) {\n          if (val == null || val.trim().isEmpty) {\n            return '${localizations.redirect} URL ${localizations.cannotBeEmpty}';\n          }\n          return null;\n        });\n  }\n\n  Widget textField(String label, dynamic value, String hint, {ValueChanged<String>? onChanged}) {\n    return Row(children: [\n      SizedBox(width: 80, child: Text(label)),\n      Expanded(\n          child: TextFormField(\n        initialValue: value,\n        onChanged: onChanged,\n        decoration: InputDecoration(\n            hintText: hint,\n            hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n            contentPadding: const EdgeInsets.all(10),\n            errorStyle: const TextStyle(height: 0, fontSize: 0),\n            focusedBorder: focusedBorder(),\n            isDense: true,\n            border: const OutlineInputBorder()),\n      ))\n    ]);\n  }\n\n  Widget statusCodeEdit() {\n    var rewriteItem = items.firstWhere((item) => item.type == RewriteType.replaceResponseStatus);\n\n    return Container(\n        padding: const EdgeInsets.all(10),\n        child: Column(children: [\n          Row(crossAxisAlignment: CrossAxisAlignment.center, children: [\n            Text(localizations.statusCode),\n            const SizedBox(width: 10),\n            SizedBox(\n                width: 100,\n                child: TextFormField(\n                  style: const TextStyle(fontSize: 14),\n                  initialValue: rewriteItem.statusCode?.toString(),\n                  onChanged: (val) => rewriteItem.statusCode = int.tryParse(val),\n                  inputFormatters: [FilteringTextInputFormatter.digitsOnly],\n                  decoration: InputDecoration(\n                      contentPadding: const EdgeInsets.all(10),\n                      focusedBorder: focusedBorder(),\n                      isDense: true,\n                      border: const OutlineInputBorder()),\n                )),\n            Expanded(\n                child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n              Text(localizations.enable),\n              const SizedBox(width: 10),\n              SwitchWidget(\n                  value: rewriteItem.enabled,\n                  scale: 0.65,\n                  onChanged: (val) => setState(() {\n                        rewriteItem.enabled = val;\n                      }))\n            ])),\n            const SizedBox(width: 10),\n          ])\n        ]));\n  }\n\n  InputDecoration decoration(String label, {String? hintText}) {\n    Color color = Theme.of(context).colorScheme.primary;\n    // Color color = Colors.blueAccent;\n    return InputDecoration(\n        floatingLabelBehavior: FloatingLabelBehavior.always,\n        labelText: label,\n        hintText: hintText,\n        isDense: true,\n        border: OutlineInputBorder(borderSide: BorderSide(width: 0.8, color: color)),\n        enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.5, color: color)),\n        focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color)));\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n\n///请求头\nclass Headers extends StatefulWidget {\n  final Map<String, String>? headers;\n\n  const Headers({super.key, this.headers});\n\n  @override\n  State<StatefulWidget> createState() {\n    return HeadersState();\n  }\n}\n\nclass HeadersState extends State<Headers> with AutomaticKeepAliveClientMixin {\n  final Map<TextEditingController, TextEditingController> _headers = {};\n\n  @override\n  bool get wantKeepAlive => true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.headers == null) {\n      return;\n    }\n\n    setHeaders(widget.headers);\n  }\n\n  setHeaders(Map<String, String>? headers) {\n    _clear();\n    headers?.forEach((name, value) {\n      _headers[TextEditingController(text: name)] = TextEditingController(text: value);\n    });\n  }\n\n  ///获取所有请求头\n  Map<String, String> getHeaders() {\n    var headers = <String, String>{};\n    _headers.forEach((name, value) {\n      if (name.text.isEmpty) {\n        return;\n      }\n      headers[name.text] = value.text;\n    });\n    return headers;\n  }\n\n  @override\n  dispose() {\n    _clear();\n    super.dispose();\n  }\n\n  _clear() {\n    _headers.forEach((key, value) {\n      key.dispose();\n      value.dispose();\n    });\n    _headers.clear();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    var list = _buildRows();\n\n    return Column(children: [\n      Expanded(\n          child: Padding(\n              padding: const EdgeInsets.only(top: 10, bottom: 10),\n              child: ListView.separated(\n                  shrinkWrap: true,\n                  separatorBuilder: (context, index) =>\n                      index == list.length ? const SizedBox() : const Divider(thickness: 0.2),\n                  itemBuilder: (context, index) => list[index],\n                  itemCount: list.length))),\n      TextButton(\n        child: Text(\"${localizations.add}Header\", textAlign: TextAlign.center),\n        onPressed: () {\n          setState(() {\n            _headers[TextEditingController()] = TextEditingController();\n          });\n        },\n      ),\n    ]);\n  }\n\n  List<Widget> _buildRows() {\n    List<Widget> list = [];\n\n    _headers.forEach((key, val) {\n      list.add(_row(\n          _cell(key, isKey: true),\n          _cell(val),\n          Padding(\n              padding: const EdgeInsets.only(right: 15),\n              child: InkWell(\n                  onTap: () {\n                    setState(() {\n                      _headers.remove(key);\n                    });\n                  },\n                  child: const Icon(Icons.remove_circle_outline, size: 16)))));\n    });\n\n    return list;\n  }\n\n  Widget _cell(TextEditingController val, {bool isKey = false}) {\n    return Container(\n        padding: const EdgeInsets.only(right: 5),\n        child: TextFormField(\n            style: TextStyle(fontSize: 12, fontWeight: isKey ? FontWeight.w500 : null),\n            controller: val,\n            minLines: 1,\n            maxLines: 3,\n            decoration: InputDecoration(\n                isDense: true,\n                border: InputBorder.none,\n                hintStyle: TextStyle(fontSize: 12, color: Colors.grey),\n                hintText: isKey ? \"Key\" : \"Value\")));\n  }\n\n  Widget _row(Widget key, Widget val, Widget? op) {\n    return Row(children: [\n      Expanded(flex: 4, child: key),\n      const Text(\": \", style: TextStyle(color: Colors.deepOrangeAccent)),\n      Expanded(flex: 6, child: val),\n      op ?? const SizedBox()\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/rewrite/rewrite_update.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'package:flutter/material.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/network/http/http.dart';\n\nimport 'package:proxypin/ui/component/text_field.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/utils/lang.dart';\n\n/// @author wanghongen\n/// 2023/10/8\nclass DesktopRewriteUpdate extends StatefulWidget {\n  final RuleType ruleType;\n  final List<RewriteItem>? items;\n  final HttpRequest? request;\n\n  const DesktopRewriteUpdate({super.key, required this.ruleType, this.items, this.request});\n\n  @override\n  State<DesktopRewriteUpdate> createState() => RewriteUpdateState();\n}\n\nclass RewriteUpdateState extends State<DesktopRewriteUpdate> {\n  late RuleType ruleType;\n  List<RewriteItem> items = [];\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n\n    initItems(widget.ruleType, widget.items);\n    // WidgetsBinding.instance.addPostFrameCallback((_) {\n    //   add();\n    // });\n  }\n\n  ///初始化重写项\n  initItems(RuleType ruleType, List<RewriteItem>? items) {\n    this.ruleType = ruleType;\n    this.items.clear();\n\n    if (items != null) {\n      this.items.addAll(items);\n    }\n  }\n\n  List<RewriteItem> getItems() {\n    return items;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Row(\n          children: [\n            Text(localizations.requestRewriteRule, style: const TextStyle(fontSize: 13, color: Colors.grey)),\n            Expanded(\n                child: Row(\n              mainAxisAlignment: MainAxisAlignment.end,\n              children: [IconButton(onPressed: add, icon: const Icon(Icons.add)), const SizedBox(width: 10)],\n            ))\n          ],\n        ),\n        UpdateList(items: items, ruleType: ruleType, request: widget.request),\n      ],\n    );\n  }\n\n  add() {\n    showDialog(\n        context: context,\n        builder: (context) => RewriteUpdateAddDialog(ruleType: ruleType, request: widget.request)).then((value) {\n      if (value != null) {\n        setState(() {\n          items.add(value);\n        });\n      }\n    });\n  }\n}\n\nclass RewriteUpdateAddDialog extends StatefulWidget {\n  final RewriteItem? item;\n  final RuleType ruleType;\n  final HttpRequest? request;\n\n  const RewriteUpdateAddDialog({super.key, this.item, required this.ruleType, this.request});\n\n  @override\n  State<RewriteUpdateAddDialog> createState() => _RewriteUpdateAddState();\n}\n\nclass _RewriteUpdateAddState extends State<RewriteUpdateAddDialog> {\n  late RewriteType rewriteType;\n  GlobalKey formKey = GlobalKey<FormState>();\n  late RewriteItem rewriteItem;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n  var keyController = TextEditingController();\n  var valueController = TextEditingController();\n  var dataController = HighlightTextEditingController();\n\n  bool jsonFormatted = false;\n\n  @override\n  void initState() {\n    super.initState();\n    rewriteType = widget.item?.type ?? RewriteType.updateBody;\n    rewriteItem = widget.item ?? RewriteItem(rewriteType, true);\n    keyController.text = rewriteItem.key ?? '';\n    valueController.text = rewriteItem.value ?? '';\n\n    initTestData();\n    keyController.addListener(onInputChangeMatch);\n    dataController.addListener(onInputChangeMatch);\n  }\n\n  @override\n  void dispose() {\n    keyController.dispose();\n    valueController.dispose();\n    dataController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isDelete = rewriteType == RewriteType.removeQueryParam || rewriteType == RewriteType.removeHeader;\n    bool isUpdate =\n        [RewriteType.updateBody, RewriteType.updateHeader, RewriteType.updateQueryParam].contains(rewriteType);\n\n    String keyTips = \"\";\n    String valueTips = \"\";\n    if (isDelete) {\n      keyTips = localizations.matchRule;\n      valueTips = localizations.emptyMatchAll;\n    } else if (rewriteType == RewriteType.updateQueryParam || rewriteType == RewriteType.updateHeader) {\n      keyTips = rewriteType == RewriteType.updateQueryParam ? \"name=123\" : \"Content-Type: application/json\";\n      valueTips = rewriteType == RewriteType.updateQueryParam ? \"name=456\" : \"Content-Type: application/xml\";\n    }\n\n    var typeList = widget.ruleType == RuleType.requestUpdate ? RewriteType.updateRequest : RewriteType.updateResponse;\n\n    return AlertDialog(\n        scrollable: true,\n        titlePadding: const EdgeInsets.only(top: 10, left: 20),\n        actionsPadding: const EdgeInsets.only(right: 15, bottom: 15),\n        contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),\n        title: Text(localizations.add,\n            style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), textAlign: TextAlign.center),\n        actions: [\n          TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(localizations.cancel)),\n          TextButton(\n              onPressed: () {\n                if (!(formKey.currentState as FormState).validate()) {\n                  FlutterToastr.show(localizations.cannotBeEmpty, context, position: FlutterToastr.center);\n                  return;\n                }\n                rewriteItem.key = keyController.text;\n                rewriteItem.value = valueController.text;\n                rewriteItem.type = rewriteType;\n                Navigator.of(context).pop(rewriteItem);\n              },\n              child: Text(localizations.confirm)),\n        ],\n        content: Container(\n            width: 500,\n            constraints: const BoxConstraints(maxHeight: 420, minHeight: 400),\n            child: Form(\n                key: formKey,\n                child: Column(children: [\n                  Row(\n                    children: [\n                      Text(localizations.type),\n                      const SizedBox(width: 20),\n                      SizedBox(\n                          width: 140,\n                          child: DropdownButtonFormField<RewriteType>(\n                              value: rewriteType,\n                              focusColor: Colors.transparent,\n                              itemHeight: 48,\n                              decoration: const InputDecoration(\n                                  contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none),\n                              items: typeList\n                                  .map((e) => DropdownMenuItem(\n                                      value: e,\n                                      child: Text(e.getDescribe(localizations.localeName == 'zh'),\n                                          style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))))\n                                  .toList(),\n                              onChanged: (val) {\n                                setState(() {\n                                  rewriteType = val!;\n                                });\n                                initTestData();\n                              })),\n                    ],\n                  ),\n                  const SizedBox(height: 15),\n                  textField(isUpdate ? localizations.match : localizations.name, keyTips,\n                      controller: keyController, required: !isDelete),\n                  const SizedBox(height: 15),\n                  textField(isUpdate ? localizations.replace : localizations.value, valueTips,\n                      controller: valueController),\n                  const SizedBox(height: 10),\n                  Row(children: [\n                    Align(\n                        alignment: Alignment.centerLeft,\n                        child: Text(localizations.testData, style: const TextStyle(fontSize: 14))),\n                    const SizedBox(width: 10),\n                    if (!isMatch)\n                      Text(localizations.noChangesDetected, style: TextStyle(color: Colors.red, fontSize: 14)),\n                    Expanded(child: SizedBox()),\n                    IconButton(\n                      tooltip: 'JSON Format',\n                      icon: Icon(Icons.data_object,\n                          size: 20, color: jsonFormatted ? Theme.of(context).colorScheme.primary : null),\n                      onPressed: () {\n                        setState(() {\n                          jsonFormatted = !jsonFormatted;\n                          dataController.text =\n                              jsonFormatted ? JSON.pretty(dataController.text) : JSON.compact(dataController.text);\n                        });\n                      },\n                    ),\n                    const SizedBox(width: 5),\n                  ]),\n                  const SizedBox(height: 5),\n                  formField(localizations.enterMatchData, lines: 10, required: false, controller: dataController),\n                ]))));\n  }\n\n  initTestData() {\n    dataController.splitPattern = null;\n    dataController.highlightEnabled = rewriteType != RewriteType.addQueryParam && rewriteType != RewriteType.addHeader;\n    bool isRemove = [RewriteType.removeHeader, RewriteType.removeQueryParam].contains(rewriteType);\n\n    valueController.removeListener(onInputChangeMatch);\n    if (isRemove) {\n      valueController.addListener(onInputChangeMatch);\n    }\n\n    if (widget.request == null) return;\n\n    if (rewriteType == RewriteType.updateBody) {\n      dataController.text = (widget.ruleType == RuleType.requestUpdate\n              ? widget.request?.getBodyString()\n              : widget.request?.response?.getBodyString()) ??\n          '';\n      return;\n    }\n\n    if (rewriteType == RewriteType.updateQueryParam || rewriteType == RewriteType.removeQueryParam) {\n      dataController.splitPattern = '&';\n      dataController.text = Uri.decodeQueryComponent(widget.request?.requestUri?.query ?? '');\n      return;\n    }\n\n    if (rewriteType == RewriteType.updateHeader || rewriteType == RewriteType.removeHeader) {\n      var headerData = widget.ruleType == RuleType.requestUpdate\n          ? widget.request?.headers.toRawHeaders()\n          : widget.request?.response?.headers.toRawHeaders();\n      dataController.text = headerData ?? '';\n      return;\n    }\n\n    dataController.clear();\n  }\n\n  bool onMatch = false; //是否正在匹配\n  bool isMatch = true;\n\n  onInputChangeMatch() {\n    if (onMatch || dataController.highlightEnabled == false) {\n      return;\n    }\n    onMatch = true;\n\n    //高亮显示\n    Future.delayed(const Duration(milliseconds: 600), () {\n      onMatch = false;\n      if (dataController.text.isEmpty) {\n        if (isMatch) return;\n        setState(() {\n          isMatch = true;\n        });\n        return;\n      }\n\n      setState(() {\n        bool isRemove = [RewriteType.removeHeader, RewriteType.removeQueryParam].contains(rewriteType);\n        String key = keyController.text;\n        if (isRemove && key.isNotEmpty) {\n          if (rewriteType == RewriteType.removeHeader) {\n            key = '$key: ';\n          } else {\n            key = '$key=';\n          }\n          key = '$key${valueController.text}';\n        }\n\n        var match = dataController.highlight(key,\n            caseSensitive: rewriteType != RewriteType.updateHeader && rewriteType != RewriteType.removeHeader);\n        isMatch = match;\n      });\n    });\n  }\n\n  Widget textField(String label, String hint, {bool required = false, int? lines, TextEditingController? controller}) {\n    return Row(children: [\n      SizedBox(width: 60, child: Text(label)),\n      Expanded(child: formField(hint, required: required, lines: lines, controller: controller))\n    ]);\n  }\n\n  Widget formField(String hint, {bool required = false, int? lines, TextEditingController? controller}) {\n    return TextFormField(\n      controller: controller,\n      style: const TextStyle(fontSize: 14),\n      minLines: lines ?? 1,\n      maxLines: lines ?? 2,\n      validator: (val) => val?.isNotEmpty == true || !required ? null : \"\",\n      decoration: InputDecoration(\n          hintText: hint,\n          hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n          contentPadding: const EdgeInsets.all(10),\n          errorStyle: const TextStyle(height: 0, fontSize: 0),\n          focusedBorder: focusedBorder(),\n          isDense: true,\n          border: const OutlineInputBorder()),\n    );\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n\nclass UpdateList extends StatefulWidget {\n  final List<RewriteItem> items;\n  final RuleType ruleType;\n  final HttpRequest? request;\n\n  const UpdateList({super.key, required this.items, required this.ruleType, this.request});\n\n  @override\n  State<UpdateList> createState() => _UpdateListState();\n}\n\nclass _UpdateListState extends State<UpdateList> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n        padding: const EdgeInsets.only(top: 10),\n        constraints: const BoxConstraints(minHeight: 330),\n        decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n        child: SingleChildScrollView(\n            child: Column(children: [\n          Row(\n            mainAxisAlignment: MainAxisAlignment.start,\n            children: [\n              Container(width: 130, padding: const EdgeInsets.only(left: 10), child: Text(localizations.type)),\n              SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)),\n              const VerticalDivider(),\n              Expanded(child: Text(localizations.modify)),\n            ],\n          ),\n          const Divider(thickness: 0.5),\n          Column(children: rows(widget.items))\n        ])));\n  }\n\n  int selected = -1;\n\n  List<Widget> rows(List<RewriteItem> list) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return List.generate(list.length, (index) {\n      return InkWell(\n          highlightColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          hoverColor: primaryColor.withOpacity(0.3),\n          onDoubleTap: () => showDialog(\n                      context: context,\n                      builder: (context) =>\n                          RewriteUpdateAddDialog(item: list[index], ruleType: widget.ruleType, request: widget.request))\n                  .then((value) {\n                if (value != null) setState(() {});\n              }),\n          onSecondaryTapDown: (details) => showMenus(details, index),\n          child: Container(\n              color: selected == index\n                  ? primaryColor\n                  : index.isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 30,\n              padding: const EdgeInsets.all(5),\n              child: Row(\n                children: [\n                  SizedBox(\n                      width: 130,\n                      child: Text(list[index].type.getDescribe(isCN), style: const TextStyle(fontSize: 13))),\n                  SizedBox(\n                      width: 40,\n                      child: SwitchWidget(\n                          scale: 0.6,\n                          value: list[index].enabled,\n                          onChanged: (val) {\n                            list[index].enabled = val;\n                          })),\n                  const SizedBox(width: 20),\n                  Expanded(child: Text(getText(list[index]).fixAutoLines(), style: const TextStyle(fontSize: 13))),\n                ],\n              )));\n    });\n  }\n\n  String getText(RewriteItem item) {\n    bool isUpdate =\n        [RewriteType.updateBody, RewriteType.updateHeader, RewriteType.updateQueryParam].contains(item.type);\n    if (isUpdate) {\n      return \"${item.key} -> ${item.value}\";\n    }\n\n    return \"${item.key}=${item.value}\";\n  }\n\n  showMenus(TapDownDetails details, int index) {\n    setState(() {\n      selected = index;\n    });\n\n    showContextMenu(context, details.globalPosition, items: [\n      PopupMenuItem(\n          height: 35,\n          child: Text(localizations.edit),\n          onTap: () async {\n            showDialog(\n                context: context,\n                barrierDismissible: false,\n                builder: (BuildContext context) => RewriteUpdateAddDialog(\n                    item: widget.items[index], ruleType: widget.ruleType, request: widget.request)).then((value) {\n              if (value != null) {\n                setState(() {});\n              }\n            });\n          }),\n      PopupMenuItem(\n          height: 35,\n          child: widget.items[index].enabled ? Text(localizations.disabled) : Text(localizations.enable),\n          onTap: () => widget.items[index].enabled = !widget.items[index].enabled),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35,\n          child: Text(localizations.delete),\n          onTap: () async {\n            widget.items.removeAt(index);\n            if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n          }),\n    ]).then((value) {\n      setState(() {\n        selected = -1;\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/script.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_code_editor/flutter_code_editor.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_highlight/themes/monokai-sublime.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:highlight/languages/javascript.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:get/get.dart';\nimport 'package:proxypin/network/components/manager/script_manager.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/multi_window.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nbool _refresh = false;\n\n/// 刷新脚本\nFuture<void> _refreshScript({bool force = false}) async {\n  if (force) {\n    _refresh = false;\n    await ScriptManager.instance.then((manager) => manager.flushConfig());\n    await DesktopMultiWindow.invokeMethod(0, \"refreshScript\");\n  }\n  if (_refresh) {\n    return;\n  }\n  _refresh = true;\n  Future.delayed(const Duration(milliseconds: 1000), () async {\n    _refresh = false;\n    await ScriptManager.instance.then((manager) => manager.flushConfig());\n    await DesktopMultiWindow.invokeMethod(0, \"refreshScript\");\n  });\n}\n\n/// @author wanghongen\n/// 2023/10/8\nclass ScriptWidget extends StatefulWidget {\n  final int windowId;\n\n  const ScriptWidget({super.key, required this.windowId});\n\n  @override\n  State<ScriptWidget> createState() => _ScriptWidgetState();\n}\n\nclass _ScriptWidgetState extends State<ScriptWidget> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    HardwareKeyboard.instance.addHandler(onKeyEvent);\n  }\n\n  @override\n  void dispose() {\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if (HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.escape) && Navigator.canPop(context)) {\n      Navigator.maybePop(context);\n      return true;\n    }\n\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      if (_refresh) {\n        _refreshScript(force: true).whenComplete(() => WindowController.fromWindowId(widget.windowId).close());\n        return true;\n      }\n      WindowController.fromWindowId(widget.windowId).close();\n      return true;\n    }\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        backgroundColor: Theme.of(context).dialogBackgroundColor,\n        appBar: AppBar(\n            title: Text(localizations.script, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n            toolbarHeight: 36,\n            centerTitle: true),\n        body: Padding(\n            padding: const EdgeInsets.only(left: 15, right: 10),\n            child: futureWidget(\n                ScriptManager.instance,\n                loading: true,\n                (data) => Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        mainAxisAlignment: MainAxisAlignment.start,\n                        children: [\n                          Row(children: [\n                            SizedBox(\n                                width: 350,\n                                child: ListTile(\n                                    title: Text(localizations.enableScript),\n                                    subtitle: Text(localizations.scriptUseDescribe),\n                                    trailing: SwitchWidget(\n                                        value: data.enabled,\n                                        scale: 0.8,\n                                        onChanged: (value) {\n                                          data.enabled = value;\n                                          _refreshScript();\n                                        }))),\n                            Expanded(\n                                child: Row(\n                              mainAxisAlignment: MainAxisAlignment.end,\n                              children: [\n                                const SizedBox(width: 10),\n                                TextButton.icon(\n                                    icon: const Icon(Icons.add, size: 18),\n                                    onPressed: scriptAdd,\n                                    label: Text(localizations.add)),\n                                const SizedBox(width: 10),\n                                TextButton.icon(\n                                  icon: const Icon(Icons.input_rounded, size: 18),\n                                  onPressed: import,\n                                  label: Text(localizations.import),\n                                ),\n                                const SizedBox(width: 10),\n                                TextButton.icon(\n                                  icon: const Icon(Icons.terminal, size: 18),\n                                  onPressed: consoleLog,\n                                  label: Text(localizations.logger),\n                                ),\n                              ],\n                            )),\n                            const SizedBox(width: 15)\n                          ]),\n                          const SizedBox(height: 5),\n                          ScriptList(scripts: data.list, windowId: widget.windowId),\n                        ]))));\n  }\n\n  void consoleLog() {\n    openScriptConsoleWindow();\n  }\n\n  //导入js\n  Future<void> import() async {\n    String? path;\n    if (Platform.isMacOS) {\n      path = await DesktopMultiWindow.invokeMethod(0, \"pickFiles\", {\n        \"allowedExtensions\": ['json']\n      });\n      WindowController.fromWindowId(widget.windowId).show();\n    } else {\n      FilePickerResult? result =\n          await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']);\n      path = result?.files.single.path;\n    }\n\n    if (path == null) {\n      return;\n    }\n    try {\n      var json = jsonDecode(await File(path).readAsString());\n      var scriptManager = (await ScriptManager.instance);\n      if (json is List<dynamic>) {\n        for (var item in json) {\n          var scriptItem = ScriptItem.fromJson(item);\n          await scriptManager.addScript(scriptItem, item['script']);\n        }\n      } else {\n        var scriptItem = ScriptItem.fromJson(json);\n        await scriptManager.addScript(scriptItem, json['script']);\n      }\n\n      _refreshScript();\n      if (mounted) {\n        FlutterToastr.show(localizations.importSuccess, context);\n      }\n      setState(() {});\n    } catch (e, t) {\n      logger.e('导入失败 $path', error: e, stackTrace: t);\n      if (mounted) {\n        FlutterToastr.show(\"${localizations.importFailed} $e\", context);\n      }\n    }\n  }\n\n  /// 添加脚本\n  Future<void> scriptAdd() async {\n    showDialog(barrierDismissible: false, context: context, builder: (_) => const ScriptEdit()).then((value) {\n      if (value != null) {\n        setState(() {});\n      }\n    });\n  }\n}\n\nclass ScriptConsoleWidget extends StatefulWidget {\n  final int windowId;\n\n  const ScriptConsoleWidget({super.key, required this.windowId});\n\n  @override\n  State<ScriptConsoleWidget> createState() => _ScriptConsoleState();\n}\n\nclass _ScriptConsoleState extends State<ScriptConsoleWidget> {\n  final List<LogInfo> logs = [];\n  final ScrollController _scrollController = ScrollController();\n  bool scrollEnd = true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    DesktopMultiWindow.invokeMethod(0, \"registerConsoleLog\", widget.windowId);\n    DesktopMultiWindow.setMethodHandler((call, fromWindowId) async {\n      if (call.method == 'consoleLog') {\n        setState(() {\n          var logInfo = LogInfo(call.arguments['level'], call.arguments['output']);\n          logs.add(logInfo);\n        });\n\n        if (scrollEnd) {\n          Future.delayed(const Duration(seconds: 1), () {\n            if (mounted) {\n              _scrollController.animateTo(_scrollController.position.maxScrollExtent,\n                  duration: const Duration(milliseconds: 300), curve: Curves.easeOut);\n            }\n          });\n        }\n      }\n      return \"ok\";\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        backgroundColor: Theme.of(context).dialogBackgroundColor,\n        appBar: AppBar(\n            title: Text(localizations.logger, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n            actions: [\n              IconButton(\n                tooltip: localizations.scrollEnd,\n                onPressed: () {\n                  setState(() {\n                    scrollEnd = !scrollEnd;\n                  });\n                  if (scrollEnd) {\n                    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);\n                  }\n                },\n                icon: Icon(Icons.update, color: scrollEnd ? Theme.of(context).colorScheme.primary : null),\n              ),\n              const SizedBox(width: 10),\n              IconButton(\n                  tooltip: localizations.clear,\n                  onPressed: () => setState(() {\n                        logs.clear();\n                      }),\n                  icon: const Icon(Icons.delete)),\n              const SizedBox(width: 10)\n            ],\n            toolbarHeight: 36,\n            centerTitle: true),\n        body: Container(\n            decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.3))),\n            margin: const EdgeInsets.all(5),\n            padding: const EdgeInsets.all(5),\n            child: ListView.builder(\n              itemCount: logs.length,\n              controller: _scrollController,\n              itemBuilder: (BuildContext context, int index) {\n                Color? color;\n                if (logs[index].level == 'error') {\n                  color = Colors.red;\n                } else if (logs[index].level == 'warn') {\n                  color = Colors.orange;\n                }\n\n                //脚本日志 样式展示\n                return Padding(\n                    padding: const EdgeInsets.only(top: 5),\n                    child: Row(\n                      children: [\n                        Text(logs[index].time.format(), style: const TextStyle(fontSize: 13, color: Colors.grey)),\n                        const SizedBox(width: 10),\n                        Text(logs[index].level, style: TextStyle(fontSize: 13, color: color)),\n                        const SizedBox(width: 10),\n                        Expanded(\n                            child: SelectableText(logs[index].output, style: TextStyle(fontSize: 13, color: color))),\n                      ],\n                    ));\n              },\n            )));\n  }\n}\n\n/// 编辑脚本\nclass ScriptEdit extends StatefulWidget {\n  final ScriptItem? scriptItem;\n  final String? script;\n\n  /// Legacy single URL input; prefer [urls].\n  final String? url;\n\n  /// Optional multiple URLs input (matches mobile ScriptEdit).\n  final List<String>? urls;\n  final String? title;\n  final bool fromRemoteUrl;\n\n  const ScriptEdit({\n    super.key,\n    this.scriptItem,\n    this.script,\n    this.url,\n    this.urls,\n    this.title,\n    this.fromRemoteUrl = false,\n  });\n\n  @override\n  State<StatefulWidget> createState() => _ScriptEditState();\n}\n\nclass _ScriptEditState extends State<ScriptEdit> {\n  late CodeController script;\n  late TextEditingController nameController;\n  late List<TextEditingController> urlControllers;\n  late TextEditingController remoteUrlController;\n  late bool _useRemote;\n  final RxBool _fetchingRemoteScript = false.obs;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  Future<void> _fetchRemoteScript() async {\n    if (_fetchingRemoteScript.value) return;\n    final remoteUrl = remoteUrlController.text.trim();\n    if (remoteUrl.isEmpty) {\n      FlutterToastr.show(\"${localizations.remoteUrl} ${localizations.cannotBeEmpty}\", context, position: FlutterToastr.top);\n      return;\n    }\n\n    final uri = Uri.tryParse(remoteUrl);\n    if (uri == null || !(uri.scheme == 'http' || uri.scheme == 'https')) {\n      FlutterToastr.show(\"${localizations.remoteUrl} ${localizations.fail}\", context, position: FlutterToastr.top);\n      return;\n    }\n\n    try {\n      _fetchingRemoteScript.value = true;\n      final resp = await http.get(uri);\n      if (resp.statusCode < 200 || resp.statusCode >= 300) {\n        FlutterToastr.show(\"Fetch failed: HTTP ${resp.statusCode}\", context, position: FlutterToastr.top);\n        return;\n      }\n      script.text = resp.body;\n      if (mounted) {\n        setState(() {});\n      }\n    } catch (e) {\n      if (mounted) {\n        FlutterToastr.show(\"Fetch failed: $e\", context, position: FlutterToastr.top);\n      }\n    } finally {\n      _fetchingRemoteScript.value = false;\n    }\n  }\n\n  void _resetScript() {\n    script.text = ScriptManager.template;\n    script.text = ScriptManager.template;\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    script = CodeController(language: javascript, text: widget.script ?? ScriptManager.template);\n    nameController = TextEditingController(text: widget.scriptItem?.name ?? widget.title);\n    remoteUrlController = TextEditingController(text: widget.scriptItem?.remoteUrl ?? '');\n    _useRemote = widget.fromRemoteUrl || ((widget.scriptItem?.remoteUrl ?? '').trim().isNotEmpty);\n    final urls = widget.scriptItem?.urls ??\n        (widget.urls != null && widget.urls!.isNotEmpty\n            ? widget.urls!\n            : (widget.url != null && widget.url!.isNotEmpty ? [widget.url!] : <String>[]));\n    urlControllers =\n        urls.isNotEmpty ? urls.map((u) => TextEditingController(text: u)).toList() : [TextEditingController()];\n  }\n\n  @override\n  void dispose() {\n    script.dispose();\n    nameController.dispose();\n    remoteUrlController.dispose();\n    for (final c in urlControllers) {\n      c.dispose();\n    }\n\n    _fetchingRemoteScript.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    GlobalKey formKey = GlobalKey<FormState>();\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return AlertDialog(\n      scrollable: true,\n      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),\n      titlePadding: const EdgeInsets.only(left: 15, top: 6, right: 15),\n      title: Row(children: [\n        Text(localizations.scriptEdit, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n        const SizedBox(width: 10),\n        Text.rich(TextSpan(\n            text: localizations.useGuide,\n            style: const TextStyle(color: Colors.blue, fontSize: 14),\n            recognizer: TapGestureRecognizer()\n              ..onTap = () => DesktopMultiWindow.invokeMethod(\n                  0,\n                  \"launchUrl\",\n                  isCN\n                      ? 'https://gitee.com/wanghongenpin/proxypin/wikis/%E8%84%9A%E6%9C%AC'\n                      : 'https://github.com/wanghongenpin/proxypin/wiki/Script'))),\n        const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))\n      ]),\n      contentPadding: const EdgeInsets.only(left: 15, right: 15),\n      actionsPadding: const EdgeInsets.only(right: 10, bottom: 10),\n      actions: [\n        ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(localizations.cancel)),\n        FilledButton(\n            onPressed: () async {\n              if (!(formKey.currentState as FormState).validate()) {\n                FlutterToastr.show(\"${localizations.name} URL ${localizations.cannotBeEmpty}\", context,\n                    position: FlutterToastr.top);\n                return;\n              }\n              final urls = urlControllers.map((c) => c.text.trim()).where((u) => u.isNotEmpty).toSet().toList();\n              if (urls.isEmpty) {\n                FlutterToastr.show(\"URL ${localizations.cannotBeEmpty}\", context, position: FlutterToastr.top);\n                return;\n              }\n\n              // Only persist remoteUrl when remote mode is enabled.\n              final remoteUrl = _useRemote ? remoteUrlController.text.trim() : '';\n              final hasRemote = remoteUrl.isNotEmpty;\n              if (_useRemote && !hasRemote) {\n                FlutterToastr.show(\"${localizations.remoteUrl} ${localizations.cannotBeEmpty}\", context, position: FlutterToastr.top);\n                return;\n              }\n\n              if (widget.scriptItem == null) {\n                var scriptItem = ScriptItem(true, nameController.text, urls);\n                scriptItem.remoteUrl = _useRemote ? remoteUrl : null;\n                await (await ScriptManager.instance).addScript(scriptItem, script.text);\n              } else {\n                widget.scriptItem?.name = nameController.text;\n                widget.scriptItem?.urls = urls;\n                widget.scriptItem?.urlRegs = null;\n                widget.scriptItem?.remoteUrl = _useRemote ? remoteUrl : null;\n                (await ScriptManager.instance).updateScript(widget.scriptItem!, script.text);\n              }\n              _refreshScript();\n              if (context.mounted) {\n                Navigator.of(context).maybePop(true);\n              }\n            },\n            child: Text(localizations.save)),\n      ],\n      content: Form(\n          key: formKey,\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              // Name section\n              Card(\n                  color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),\n                  elevation: 0,\n                  shape: RoundedRectangleBorder(\n                      side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)),\n                      borderRadius: BorderRadius.circular(8)),\n                  child: Padding(\n                      padding: const EdgeInsets.all(10),\n                      child: textField(\"${localizations.name}:\", nameController, localizations.pleaseEnter))),\n\n              // URLs section\n              Card(\n                  color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),\n                  elevation: 0,\n                  shape: RoundedRectangleBorder(\n                      side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)),\n                      borderRadius: BorderRadius.circular(8)),\n                  child: Padding(\n                      padding: const EdgeInsets.symmetric(horizontal: 10),\n                      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n                        Row(children: [\n                          const Text(\"URL(s):\"),\n                          const SizedBox(width: 8),\n                          IconButton(\n                              icon: const Icon(Icons.add_outlined, size: 20),\n                              tooltip: localizations.add,\n                              onPressed: () {\n                                setState(() {\n                                  urlControllers.add(TextEditingController());\n                                });\n                              }),\n                          const Spacer(),\n                          Text(\"${urlControllers.length}\", style: const TextStyle(fontSize: 12, color: Colors.grey))\n                        ]),\n                        const SizedBox(height: 6),\n                        ...List.generate(\n                            urlControllers.length,\n                            (i) => Padding(\n                                padding: const EdgeInsets.only(bottom: 8),\n                                child: Row(children: [\n                                  Expanded(\n                                      child: TextFormField(\n                                    controller: urlControllers[i],\n                                    validator: (val) => val?.isNotEmpty == true ? null : \"\",\n                                    keyboardType: TextInputType.url,\n                                    decoration: InputDecoration(\n                                      hintText: \"github.com/api/*\",\n                                      hintStyle: const TextStyle(fontSize: 14, color: Colors.grey),\n                                      contentPadding: const EdgeInsets.all(10),\n                                      errorStyle: const TextStyle(height: 0, fontSize: 0),\n                                      focusedBorder: focusedBorder(),\n                                      isDense: true,\n                                      border: const OutlineInputBorder(),\n                                    ),\n                                  )),\n                                  if (urlControllers.length > 1)\n                                    IconButton(\n                                        icon: const Icon(Icons.remove_circle_outline, color: Colors.red),\n                                        tooltip: localizations.delete,\n                                        onPressed: () {\n                                          setState(() {\n                                            urlControllers[i].dispose();\n                                            urlControllers.removeAt(i);\n                                          });\n                                        }),\n                                ])))\n                      ]))),\n\n              // Script section\n              Card(\n                  color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),\n                  elevation: 0,\n                  shape: RoundedRectangleBorder(\n                      side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)),\n                      borderRadius: BorderRadius.circular(8)),\n                  child: Padding(\n                      padding: const EdgeInsets.all(6),\n                      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n                        Row(children: [\n                          Text(\"${localizations.script}:\", style: const TextStyle(fontWeight: FontWeight.w500)),\n                          const SizedBox(width: 12),\n                          SizedBox(\n                            width: 155,\n                            height: 34,\n                            child: DropdownButtonFormField<bool>(\n                              initialValue: _useRemote,\n                              items: [\n                                DropdownMenuItem(value: false, child: Text(localizations.local)),\n                                DropdownMenuItem(value: true, child: Text(localizations.remoteUrl)),\n                              ],\n                              onChanged: (val) {\n                                if (val == null) return;\n                                setState(() {\n                                  _useRemote = val;\n                                });\n                              },\n                              decoration: InputDecoration(\n                                contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),\n                                focusedBorder: focusedBorder(),\n                                isDense: true,\n                                border: const OutlineInputBorder(),\n                              ),\n                            ),\n                          ),\n\n                          // Put Remote URL right after type selector.\n                          if (_useRemote) ...[\n                            const SizedBox(width: 10),\n                            Expanded(\n                              flex: 6,\n                              child: SizedBox(\n                                height: 34,\n                                child: TextFormField(\n                                  controller: remoteUrlController,\n                                  keyboardType: TextInputType.url,\n                                  decoration: InputDecoration(\n                                    hintText: 'https://example.com/script.js',\n                                    hintStyle: const TextStyle(fontSize: 14, color: Colors.grey),\n                                    contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),\n                                    focusedBorder: focusedBorder(),\n                                    isDense: true,\n                                    border: const OutlineInputBorder(),\n                                  ),\n                                  onFieldSubmitted: (_) => _fetchRemoteScript(),\n                                ),\n                              ),\n                            ),\n                            const SizedBox(width: 8),\n                            Obx(() => FilledButton.tonal(\n                                  style: ElevatedButton.styleFrom(\n                                      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10)),\n                                  onPressed: _fetchingRemoteScript.value ? null : _fetchRemoteScript,\n                                  child: _fetchingRemoteScript.value\n                                      ? const SizedBox(\n                                          width: 16,\n                                          height: 16,\n                                          child: CircularProgressIndicator(strokeWidth: 2),\n                                        )\n                                      : Text(localizations.view),\n                                )),\n                          ],\n\n                          const Spacer(),\n                          Tooltip(\n                              message: localizations.copy,\n                              child: IconButton(\n                                  icon: const Icon(Icons.copy_all_outlined, size: 20),\n                                  onPressed: () {\n                                    Clipboard.setData(ClipboardData(text: script.text));\n                                    FlutterToastr.show(localizations.copied, context, position: FlutterToastr.top);\n                                  })),\n                          Tooltip(\n                              message: 'Reset',\n                              child: IconButton(\n                                  icon: const Icon(Icons.settings_backup_restore, size: 22),\n                                  onPressed: _resetScript)),\n                          const SizedBox(width: 5)\n                        ]),\n                        const SizedBox(height: 8),\n                        SizedBox(\n                            width: 850,\n                            height: 380,\n                            child: CodeTheme(\n                                data: CodeThemeData(styles: monokaiSublimeTheme),\n                                child: ClipRRect(\n                                    borderRadius: BorderRadius.circular(6),\n                                    child: Container(\n                                        decoration: BoxDecoration(\n                                            color: Colors.grey.shade900,\n                                            border: Border.all(color: Colors.grey.withOpacity(0.2))),\n                                        child: SingleChildScrollView(\n                                            child: CodeField(\n                                          readOnly: _useRemote,\n                                          textStyle: const TextStyle(fontSize: 13, color: Colors.white),\n                                          controller: script,\n                                          gutterStyle: const GutterStyle(width: 50, margin: 0),\n                                        ))))))\n                      ])))\n            ],\n          )),\n    );\n  }\n\n  Widget textField(String label, TextEditingController controller, String hint, {TextInputType? keyboardType}) {\n    return Row(children: [\n      SizedBox(width: 50, child: Text(label)),\n      Expanded(\n          child: TextFormField(\n        controller: controller,\n        validator: (val) => val?.isNotEmpty == true ? null : \"\",\n        keyboardType: keyboardType,\n        decoration: InputDecoration(\n            hintText: hint,\n            hintStyle: const TextStyle(fontSize: 14, color: Colors.grey),\n            contentPadding: const EdgeInsets.all(10),\n            errorStyle: const TextStyle(height: 0, fontSize: 0),\n            focusedBorder: focusedBorder(),\n            isDense: true,\n            border: const OutlineInputBorder()),\n      ))\n    ]);\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n\n/// 脚本列表\nclass ScriptList extends StatefulWidget {\n  final int windowId;\n  final List<ScriptItem> scripts;\n\n  const ScriptList({super.key, required this.scripts, required this.windowId});\n\n  @override\n  State<ScriptList> createState() => _ScriptListState();\n}\n\nclass _ScriptListState extends State<ScriptList> {\n  Set<int> selected = {};\n  bool isPressed = false;\n  Offset? lastPressPosition;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n        onSecondaryTap: () {\n          if (lastPressPosition == null) {\n            return;\n          }\n          showGlobalMenu(lastPressPosition!);\n        },\n        onTapDown: (details) {\n          if (selected.isEmpty) {\n            return;\n          }\n          if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n            return;\n          }\n          setState(() {\n            selected.clear();\n          });\n        },\n        child: Listener(\n            onPointerUp: (event) => isPressed = false,\n            onPointerDown: (event) {\n              lastPressPosition = event.localPosition;\n              if (event.buttons == kPrimaryMouseButton) {\n                isPressed = true;\n              }\n            },\n            child: Container(\n                padding: const EdgeInsets.only(top: 10),\n                height: 630,\n                decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n                child: SingleChildScrollView(\n                    child: Column(children: [\n                  Row(mainAxisAlignment: MainAxisAlignment.start, children: [\n                    Container(width: 200, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)),\n                    SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)),\n                    const VerticalDivider(),\n                    const Expanded(child: Text(\"URL\")),\n                  ]),\n                  const Divider(thickness: 0.5),\n                  Column(children: rows(widget.scripts))\n                ])))));\n  }\n\n  List<Widget> rows(List<ScriptItem> list) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n\n    return List.generate(list.length, (index) {\n      final item = list[index];\n      final isRemote = item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty;\n      return InkWell(\n          // onTap: () {\n          //   selected[index] = !(selected[index] ?? false);\n          //   setState(() {});\n          // },\n          highlightColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          hoverColor: primaryColor.withOpacity(0.3),\n          onDoubleTap: () => showEdit(index),\n          onSecondaryTapDown: (details) => showMenus(details, index),\n          onHover: (hover) {\n            if (isPressed && !selected.contains(index)) {\n              setState(() {\n                selected.add(index);\n              });\n            }\n          },\n          onTap: () {\n            if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {\n              setState(() {\n                selected.contains(index) ? selected.remove(index) : selected.add(index);\n              });\n              return;\n            }\n            if (selected.isEmpty) {\n              return;\n            }\n            setState(() {\n              selected.clear();\n            });\n          },\n          child: Container(\n              color: selected.contains(index)\n                  ? primaryColor.withOpacity(0.6)\n                  : index.isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 30,\n              padding: const EdgeInsets.all(5),\n              child: Row(\n                children: [\n                  SizedBox(\n                      width: 200,\n                      child: Row(children: [\n                        Expanded(child: Text(item.name!, style: const TextStyle(fontSize: 13))),\n                        if (isRemote)\n                          const Padding(\n                              padding: EdgeInsets.only(left: 6),\n                              child: Text('R', style: TextStyle(fontSize: 11, color: Colors.blue))),\n                      ])),\n                  SizedBox(\n                      width: 40,\n                      child: Transform.scale(\n                          scale: 0.6,\n                          child: SwitchWidget(\n                              value: item.enabled,\n                              onChanged: (val) {\n                                item.enabled = val;\n                                _refreshScript();\n                              }))),\n                  const SizedBox(width: 20),\n                  Expanded(child: Text(item.urls.join(', '), style: const TextStyle(fontSize: 13))),\n                ],\n              )));\n    });\n  }\n\n  void showGlobalMenu(Offset offset) {\n    showContextMenu(context, offset, items: [\n      PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()),\n      PopupMenuItem(height: 35, child: Text(localizations.export), onTap: () => export(selected.toList())),\n      const PopupMenuDivider(),\n      PopupMenuItem(height: 35, child: Text(localizations.enableSelect), onTap: () => enableStatus(true)),\n      PopupMenuItem(height: 35, child: Text(localizations.disableSelect), onTap: () => enableStatus(false)),\n      const PopupMenuDivider(),\n      PopupMenuItem(height: 35, child: Text(localizations.deleteSelect), onTap: () => removeScripts(selected.toList())),\n    ]);\n  }\n\n  //点击菜单\n  void showMenus(TapDownDetails details, int index) {\n    if (selected.length > 1) {\n      showGlobalMenu(details.globalPosition);\n      return;\n    }\n    setState(() {\n      selected.add(index);\n    });\n\n    showContextMenu(context, details.globalPosition, items: [\n      PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)),\n      PopupMenuItem(height: 35, child: Text(localizations.export), onTap: () => export([index])),\n      PopupMenuItem(\n          height: 35,\n          child: widget.scripts[index].enabled ? Text(localizations.disabled) : Text(localizations.enable),\n          onTap: () {\n            widget.scripts[index].enabled = !widget.scripts[index].enabled;\n            _refreshScript();\n          }),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35,\n          child: Text(localizations.delete),\n          onTap: () async {\n            var scriptManager = await ScriptManager.instance;\n            await scriptManager.removeScript(index);\n            _refreshScript();\n          }),\n    ]).then((value) {\n      if (mounted) {\n        setState(() {\n          selected.remove(index);\n        });\n      }\n    });\n  }\n\n  Future<void> showEdit([int? index]) async {\n    String? script;\n    if (index != null) {\n      var scriptManager = await ScriptManager.instance;\n      var scriptItem = widget.scripts[index];\n      if (scriptItem.remoteUrl == null || scriptItem.remoteUrl?.isEmpty == true) {\n        script = await scriptManager.getScript(scriptItem);\n      }\n    }\n    if (!mounted) {\n      return;\n    }\n\n    showDialog(\n            barrierDismissible: false,\n            context: context,\n            builder: (_) => ScriptEdit(scriptItem: index == null ? null : widget.scripts[index], script: script))\n        .then((value) {\n      if (value != null) {\n        setState(() {});\n      }\n    });\n  }\n\n  //导出js\n  Future<void> export(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    //文件名称\n    String fileName = 'proxypin-scripts.json';\n    String? path;\n    if (Platform.isMacOS) {\n      path = await DesktopMultiWindow.invokeMethod(0, \"saveFile\", {\"fileName\": fileName});\n      WindowController.fromWindowId(widget.windowId).show();\n    } else {\n      path = await FilePicker.platform.saveFile(fileName: fileName);\n    }\n    if (path == null) {\n      return;\n    }\n    var scriptManager = await ScriptManager.instance;\n    List<dynamic> json = [];\n    for (var idx in indexes) {\n      var item = widget.scripts[idx];\n      var map = item.toJson();\n      map.remove(\"scriptPath\");\n\n      if (item.remoteUrl == null || item.remoteUrl!.trim().isEmpty) {\n        map['script'] = await scriptManager.getScript(item);\n      }\n\n      json.add(map);\n    }\n\n    await File(path).writeAsBytes(utf8.encode(jsonEncode(json)));\n    if (mounted) FlutterToastr.show(localizations.exportSuccess, context);\n  }\n\n  void enableStatus(bool enable) {\n    for (var idx in selected) {\n      widget.scripts[idx].enabled = enable;\n    }\n    setState(() {});\n    _refreshScript();\n  }\n\n  Future<void> removeScripts(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    showConfirmDialog(context, content: localizations.confirmContent, onConfirm: () async {\n      var scriptManager = await ScriptManager.instance;\n      for (var idx in indexes) {\n        await scriptManager.removeScript(idx);\n      }\n\n      setState(() {\n        selected.clear();\n      });\n      _refreshScript();\n\n      if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/setting/setting.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/components/manager/hosts_manager.dart';\nimport 'package:proxypin/network/components/manager/request_block_manager.dart';\nimport 'package:proxypin/network/util/system_proxy.dart';\nimport 'package:proxypin/ui/component/multi_window.dart';\nimport 'package:proxypin/ui/component/proxy_port_setting.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/desktop/setting/about.dart';\nimport 'package:proxypin/ui/desktop/setting/external_proxy.dart';\nimport 'package:proxypin/ui/desktop/setting/hosts.dart';\nimport 'package:proxypin/ui/desktop/setting/request_block.dart';\n\nimport 'filter.dart';\n\n///设置菜单\n/// @author wanghongen\n/// 2023/10/8\nclass Setting extends StatefulWidget {\n  final ProxyServer proxyServer;\n\n  const Setting({super.key, required this.proxyServer});\n\n  @override\n  State<Setting> createState() => _SettingState();\n}\n\nclass _SettingState extends State<Setting> {\n  late Configuration configuration;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    configuration = widget.proxyServer.configuration;\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MenuAnchor(\n      builder: (context, controller, child) {\n        return IconButton(\n            icon: const Icon(Icons.settings, size: 21),\n            tooltip: localizations.setting,\n            onPressed: () {\n              if (controller.isOpen) {\n                controller.close();\n              } else {\n                controller.open();\n              }\n            });\n      },\n      menuChildren: [\n        _ProxyMenu(proxyServer: widget.proxyServer),\n        item(localizations.domainFilter, onPressed: hostFilter),\n        item(localizations.hosts, onPressed: hosts),\n        item(localizations.requestBlock, onPressed: showRequestBlock),\n        item(localizations.requestRewrite, onPressed: requestRewrite),\n        item(localizations.requestMap, onPressed: requestMap),\n        item(localizations.requestCrypto, onPressed: showRequestCrypto),\n        item(localizations.script,\n            onPressed: () => MultiWindow.openWindow(localizations.script, 'ScriptWidget', size: const Size(800, 780))),\n        item(localizations.breakpoint, onPressed: requestBreakpoint),\n        item(localizations.externalProxy, onPressed: setExternalProxy),\n        item(localizations.about, onPressed: showAbout),\n      ],\n    );\n  }\n\n  Widget item(String text, {VoidCallback? onPressed}) {\n    return MenuItemButton(\n        trailingIcon: const Icon(Icons.arrow_right),\n        onPressed: onPressed,\n        child: Padding(\n            padding: const EdgeInsets.only(left: 10, right: 5),\n            child: Text(text, style: const TextStyle(fontSize: 14))));\n  }\n\n  void showAbout() {\n    showDialog(context: context, builder: (context) => DesktopAbout());\n  }\n\n  ///设置外部代理地址\n  void setExternalProxy() {\n    showDialog(\n        barrierDismissible: false,\n        context: context,\n        builder: (context) {\n          return ExternalProxyDialog(configuration: widget.proxyServer.configuration);\n        });\n  }\n\n  ///请求重写Dialog\n  void requestRewrite() async {\n    MultiWindow.openWindow(localizations.requestRewrite, 'RequestRewriteWidget', size: const Size(800, 750));\n  }\n\n  void requestBreakpoint() async {\n    MultiWindow.openWindow(localizations.breakpoint, 'RequestBreakpointPage', size: const Size(800, 750));\n  }\n\n  ///请求本地映射\n  void requestMap() async {\n    if (!mounted) return;\n    MultiWindow.openWindow(localizations.requestMap, 'RequestMapPage', size: const Size(800, 720));\n  }\n\n  ///show域名过滤Dialog\n  void hostFilter() {\n    showDialog(\n        barrierDismissible: false, context: context, builder: (context) => FilterDialog(configuration: configuration));\n  }\n\n  ///show域名过滤Dialog\n  void hosts() async {\n    var hosts = await HostsManager.instance;\n    if (!mounted) return;\n    showDialog(barrierDismissible: false, context: context, builder: (context) => HostsDialog(hostsManager: hosts));\n  }\n\n  //请求屏蔽\n  void showRequestBlock() async {\n    var requestBlockManager = await RequestBlockManager.instance;\n    if (!mounted) return;\n    showDialog(\n        barrierDismissible: false,\n        context: context,\n        builder: (context) => RequestBlock(requestBlockManager: requestBlockManager));\n  }\n\n  void showRequestCrypto() {\n    MultiWindow.openWindow(localizations.requestCrypto, 'RequestCryptoPage', size: const Size(820, 750));\n  }\n}\n\n///代理菜单\nclass _ProxyMenu extends StatefulWidget {\n  final ProxyServer proxyServer;\n\n  const _ProxyMenu({required this.proxyServer});\n\n  @override\n  State<StatefulWidget> createState() => _ProxyMenuState();\n}\n\nclass _ProxyMenuState extends State<_ProxyMenu> {\n  var textEditingController = TextEditingController();\n\n  late Configuration configuration;\n  bool changed = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    configuration = widget.proxyServer.configuration;\n    textEditingController.text = configuration.proxyPassDomains;\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    if (configuration.proxyPassDomains != textEditingController.text) {\n      changed = true;\n      configuration.proxyPassDomains = textEditingController.text;\n      SystemProxy.setProxyPassDomains(configuration.proxyPassDomains);\n    }\n\n    if (changed) {\n      configuration.flushConfig();\n    }\n    textEditingController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isEn = localizations.localeName.startsWith(\"en\");\n    return SubmenuButton(\n      menuChildren: [\n        PortWidget(proxyServer: widget.proxyServer, textStyle: const TextStyle(fontSize: 13)),\n        const Divider(thickness: 0.3, height: 8),\n        setSystemProxy(),\n        const Divider(thickness: 0.3, height: 8),\n        Row(children: [\n          Expanded(\n              child: Padding(\n                  padding: const EdgeInsets.only(left: 15),\n                  child: Text(\"SOCKS5\", style: const TextStyle(fontSize: 14)))),\n          SwitchWidget(\n              value: configuration.enableSocks5,\n              scale: 0.75,\n              onChanged: (val) {\n                configuration.enableSocks5 = val;\n                changed = true;\n              }),\n          SizedBox(width: 10)\n        ]),\n        const Divider(thickness: 0.3, height: 8),\n        Row(children: [\n          Expanded(\n              child: Padding(\n                  padding: const EdgeInsets.only(left: 15),\n                  child: Text(localizations.enabledHTTP2, style: const TextStyle(fontSize: 14)))),\n          SwitchWidget(\n              value: configuration.enabledHttp2,\n              scale: 0.75,\n              onChanged: (val) {\n                configuration.enabledHttp2 = val;\n                changed = true;\n              }),\n          SizedBox(width: 10)\n        ]),\n        const Divider(thickness: 0.3, height: 8),\n        const SizedBox(height: 3),\n        Padding(\n            padding: const EdgeInsets.only(left: 15),\n            child: Row(children: [\n              Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Text(localizations.proxyIgnoreDomain, style: const TextStyle(fontSize: 14)),\n                  const SizedBox(height: 3),\n                  Text(isEn ? \"Use ';' to separate multiple entries\": \"多个使用;分割\", style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),\n                ],\n              ),\n              Padding(\n                  padding: const EdgeInsets.only(left: 35),\n                  child: TextButton(\n                    child: Text(localizations.reset),\n                    onPressed: () {\n                      textEditingController.text = SystemProxy.proxyPassDomains;\n                    },\n                  ))\n            ])),\n        const SizedBox(height: 5),\n        Padding(\n            padding: const EdgeInsets.only(left: 15, right: 5),\n            child: TextField(\n                textInputAction: TextInputAction.done,\n                style: const TextStyle(fontSize: 13),\n                controller: textEditingController,\n                decoration: const InputDecoration(\n                    contentPadding: EdgeInsets.all(10),\n                    border: OutlineInputBorder(),\n                    constraints: BoxConstraints(minWidth: 190, maxWidth: 190)),\n                maxLines: 5,\n                minLines: 1)),\n        const SizedBox(height: 10),\n      ],\n      child: Padding(\n          padding: const EdgeInsets.only(left: 10),\n          child: Text(localizations.proxy, style: const TextStyle(fontSize: 14))),\n    );\n  }\n\n  ///设置系统代理\n  Widget setSystemProxy() {\n    return Row(children: [\n      Expanded(\n          child: Padding(\n              padding: const EdgeInsets.only(left: 15, right: 20),\n              child: Text(localizations.setAs + localizations.systemProxy, style: const TextStyle(fontSize: 14)))),\n      Transform.scale(\n          scale: 0.75,\n          child: Switch(\n              hoverColor: Colors.transparent,\n              value: configuration.enableSystemProxy,\n              onChanged: (val) {\n                widget.proxyServer.setSystemProxyEnable(val);\n                configuration.enableSystemProxy = val;\n                setState(() {\n                  changed = true;\n                });\n              })),\n      SizedBox(width: 10)\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/ssl/cert_installer.dart",
    "content": "import 'dart:io';\nimport 'package:proxypin/network/util/cert/cert_data.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nclass CertInstaller {\n  static Future<bool> installCertificate(File certFile) async {\n    try {\n      if (Platform.isMacOS) {\n        // 使用 security add-trusted-cert 安装证书到登录钥匙串并设为信任根\n        final result = await Process.run('security', [\n          'add-trusted-cert',\n          '-r',\n          'trustRoot',\n          '-k',\n          '${Platform.environment['HOME']}/Library/Keychains/login.keychain-db',\n          certFile.path,\n        ]);\n        logger.d('security add-trusted-cert result: \\\\${result.stdout} \\\\${result.stderr}');\n        return result.exitCode == 0;\n      }\n\n      if (Platform.isWindows) {\n        // Windows: 使用 certutil 命令行安装证书到根证书存储区\n        final result = await Process.run('certutil', [\n          '-addstore',\n          '-user',\n          'Root',\n          certFile.path,\n        ]);\n        logger.d('certutil addstore result: \\\\${result.stdout} \\\\${result.stderr}');\n        return result.exitCode == 0;\n      }\n\n      if (Platform.isLinux) {\n        // Linux: 拷贝到 /usr/local/share/ca-certificates/ 并更新证书\n        final certName = certFile.uri.pathSegments.last.endsWith('.crt')\n            ? certFile.uri.pathSegments.last\n            : '${certFile.uri.pathSegments.last}.crt';\n        final destPath = '/usr/local/share/ca-certificates/$certName';\n        await certFile.copy(destPath);\n        final result = await Process.run('update-ca-certificates', []);\n        logger.d('update-ca-certificates result: \\\\${result.stdout} \\\\${result.stderr}');\n        return result.exitCode == 0;\n      }\n\n      // 其他平台暂不支持\n      return false;\n    } catch (e) {\n      logger.e('Failed to install certificate: $e');\n      return false;\n    }\n  }\n\n  /// 检查证书是否已安装\n  static Future<bool> isCertInstalled(File filePath, X509CertificateData caCert) async {\n    String commonName = caCert.subject['2.5.4.3'] ?? 'ProxyPin CA';\n    String? sha1 = caCert.sha1Thumbprint;\n    logger.d('Checking if certificate is installed: CN=$commonName, SHA1=$sha1');\n    try {\n      if (Platform.isWindows) {\n        List<String> args = ['-user', '-store', 'root'];\n        if (sha1 != null) {\n          args.add(sha1);\n        }\n        var res = await Process.run('certutil', args);\n        return res.stdout.toString().toLowerCase().contains(commonName.toLowerCase());\n      } else if (Platform.isMacOS) {\n        var res = await Process.run('security', ['find-certificate', '-c', commonName]);\n\n        if ((res.stdout as String).isNotEmpty) {\n          // check if trusted\n          var trustRes = await Process.run('security', ['verify-cert', '-c', filePath.path]);\n\n          logger.d('security verify-cert $commonName result: ${trustRes.stdout} ${trustRes.stderr}');\n          return (trustRes.stdout as String).contains('certificate verification successful');\n        }\n        return false;\n      } else if (Platform.isLinux) {\n        // 只检查 /usr/local/share/ca-certificates/ 下是否有对应证书文件\n        final certName = filePath.uri.pathSegments.last.endsWith('.crt')\n            ? filePath.uri.pathSegments.last\n            : '${filePath.uri.pathSegments.last}.crt';\n\n        var paths = [\n          '/usr/local/share/ca-certificates/$certName',\n          '/etc/ssl/certs/$certName',\n        ];\n        for (var p in paths) {\n          if (await File(p).exists()) return true;\n        }\n        return false;\n      }\n    } catch (_) {}\n    return false;\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/ssl/pc_cert.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/util/cert/cert_data.dart';\nimport 'package:proxypin/network/util/crts.dart';\nimport 'package:proxypin/storage/local_storage.dart';\nimport 'package:proxypin/ui/component/app_dialog.dart';\nimport 'package:proxypin/ui/desktop/ssl/cert_installer.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nimport '../../../storage/shared_preference_keys.dart';\n\nclass PCCertChecker {\n  static bool checked = false;\n\n  static void check(BuildContext context) async {\n    if (checked || !Platforms.isDesktop()) {\n      return;\n    }\n\n    checked = true;\n\n    if (ProxyServer.current?.enableSsl != true || ProxyServer.current?.configuration.enableSystemProxy != true) {\n      return;\n    }\n\n    if (_AutomaticInstallState.isCertInstalled.value == true) {\n      return;\n    }\n    if ((await LocalStorage.getBool(SharedPreferenceKeys.CERT_INSTALL_SKIP)) == true) {\n      return;\n    }\n\n    final cert = await CertificateManager.getCertificateDetails();\n    final caFile = await CertificateManager.certificateFile();\n    final installed = await CertInstaller.isCertInstalled(caFile, cert);\n    if (!installed && context.mounted) {\n      showDialog(\n          barrierDismissible: false,\n          context: context,\n          builder: (context) {\n            final localizations = AppLocalizations.of(context)!;\n            return AlertDialog(\n              content: _AutomaticInstall(),\n              actions: <Widget>[\n                TextButton(\n                  onPressed: () {\n                    LocalStorage.setBool(SharedPreferenceKeys.CERT_INSTALL_SKIP, true);\n                    Navigator.pop(context);\n                  },\n                  child: Text(localizations.appUpdateIgnoreBtnTxt),\n                ),\n                TextButton(\n                  onPressed: () => Navigator.pop(context),\n                  child: Text(localizations.cancel),\n                ),\n              ],\n            );\n          });\n    }\n  }\n}\n\nclass PCCert extends StatefulWidget {\n  const PCCert({super.key});\n\n  @override\n  State<PCCert> createState() => _PCCertState();\n}\n\nclass _PCCertState extends State<PCCert> with TickerProviderStateMixin {\n  late TabController _tabController;\n\n  @override\n  void initState() {\n    super.initState();\n    _tabController = TabController(length: 2, vsync: this);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final localizations = AppLocalizations.of(context)!;\n    final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return SimpleDialog(\n      titlePadding: const EdgeInsets.symmetric(),\n      contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 15),\n      title: Row(children: [\n        const Expanded(child: SizedBox()),\n        Text(isCN ? \"安装证书\" : \"Install Certificate\", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n        const Expanded(child: SizedBox()),\n        Align(alignment: Alignment.topRight, child: CloseButton())\n      ]),\n      children: [\n        TabBar(\n          controller: _tabController,\n          tabs: [\n            Tab(text: localizations.automatic),\n            Tab(text: localizations.manual),\n          ],\n        ),\n        SizedBox(\n          width: 700,\n          height: 470,\n          child: TabBarView(\n            controller: _tabController,\n            children: [\n              _AutomaticInstall(),\n              _buildManualTab(context),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildManualTab(BuildContext context) {\n    return SingleChildScrollView(\n      child: Padding(\n          padding: const EdgeInsets.only(top: 12.0),\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.start,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: _buildChildren(context),\n          )),\n    );\n  }\n\n  List<Widget> _buildChildren(BuildContext context) {\n    if (Platform.isMacOS || Platform.isWindows) {\n      return _buildWindowsAndMacContent(context);\n    }\n    return _buildLinuxContent(context);\n  }\n\n  List<Widget> _buildWindowsAndMacContent(BuildContext context) {\n    final localizations = AppLocalizations.of(context)!;\n    final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return [\n      isCN\n          ? Text(\" 安装证书到本系统，${Platform.isMacOS ? \"安装完双击选择“始终信任此证书”。 如安装打开失败，请导出证书拖拽到系统证书里\" : \"选择“受信任的根证书颁发机构”\"}\")\n          : Text(\n              \" Install certificate to this system，${Platform.isMacOS ? \"After installation, double-click to select “Always Trust”。\\n If installation and opening fail，Please export the certificate and drag it to the system certificate\" : \"choice“Trusted Root Certificate Authority”\"}\"),\n      const SizedBox(height: 10),\n      SizedBox(\n          width: double.maxFinite,\n          child: FilledButton(\n              onPressed: () => _manualInstallCert(),\n              style: FilledButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),\n              child: Text(localizations.installRootCa))),\n      const SizedBox(height: 10),\n      Platform.isMacOS\n          ? Image.network(\"https://foruda.gitee.com/images/1689323260158189316/c2d881a4_1073801.png\",\n              width: 800, height: 500)\n          : Row(children: [\n              Image.network(\"https://foruda.gitee.com/images/1689335589122168223/c904a543_1073801.png\",\n                  width: 370, height: 380),\n              const SizedBox(width: 10),\n              Image.network(\"https://foruda.gitee.com/images/1689335334688878324/f6aa3a3a_1073801.png\",\n                  width: 370, height: 380)\n            ])\n    ];\n  }\n\n  List<Widget> _buildLinuxContent(BuildContext context) {\n    final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return [\n      Text(isCN\n          ? \"安装证书到本系统，以Ubuntu为例 下载证书：\\n\"\n              \"先把证书复制到 /usr/local/share/ca-certificates/，然后执行 update-ca-certificates 即可。\\n\"\n              \"其他系统请网上搜索安装根证书\"\n          : \"Install the certificate to this system), take Ubuntu as an example to download the certificate:\\n\"\n              \"First copy the certificate to /usr/local/share/ca-certificates/, and then execute update-ca-certificates.\\n\"\n              \"For other systems, please search online for installing root certificates.\"),\n      const SizedBox(height: 5),\n      Text(\n          isCN\n              ? \"提示：FireFox有自己的信任证书库，所以要手动在设置中导入需要导入的证书。\"\n              : \"Note: FireFox has its own trusted certificate library, so you need to manually import the required certificates in the settings.\",\n          style: TextStyle(fontSize: 12)),\n      const SizedBox(height: 10),\n      const SelectableText.rich(\n          textAlign: TextAlign.justify,\n          TextSpan(style: TextStyle(color: Color(0xff6a8759)), children: [\n            TextSpan(text: \"  sudo cp ProxyPinCA.crt /usr/local/share/ca-certificates/ \\n\"),\n            TextSpan(text: \"  sudo update-ca-certificates\")\n          ])),\n      const SizedBox(height: 10)\n    ];\n  }\n\n  void _manualInstallCert() async {\n    var caFile = await CertificateManager.certificateFile();\n    launchUrl(Uri.file(caFile.path)).then((_) {\n      CertificateManager.cleanCache();\n    });\n  }\n}\n\nclass _AutomaticInstall extends StatefulWidget {\n  @override\n  State<StatefulWidget> createState() => _AutomaticInstallState();\n}\n\nclass _AutomaticInstallState extends State<_AutomaticInstall> {\n  static final RxnBool isCertInstalled = RxnBool(null);\n  X509CertificateData? certDetails;\n\n  @override\n  void initState() {\n    super.initState();\n    certDetails = CertificateManager.caCert;\n    _checkCertStatus();\n\n    if (certDetails == null) {\n      CertificateManager.getCertificateDetails().then((value) => setState(() {\n            certDetails = value;\n          }));\n    }\n  }\n\n  void _checkCertStatus() async {\n    final details = certDetails ?? await CertificateManager.getCertificateDetails();\n    final caFile = await CertificateManager.certificateFile();\n    isCertInstalled.value = await CertInstaller.isCertInstalled(caFile, details);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 16.0),\n      child: Obx(() => Column(mainAxisSize: MainAxisSize.min, children: buildAutomaticChildren())),\n    );\n  }\n\n  List<Widget> buildAutomaticChildren() {\n    final localizations = AppLocalizations.of(context)!;\n    final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    final subtitleStyle = Theme.of(context).textTheme.bodyMedium;\n    final infoLabelStyle = Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]);\n    final infoValueStyle = Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500);\n    List<Widget> children = [\n      const SizedBox(height: 8),\n      Text(isCN ? \"通过安装并信任 ProxyPin CA\" : \"Install and Trust ProxyPin CA Certificate\",\n          style: subtitleStyle, textAlign: TextAlign.center),\n      const SizedBox(height: 3),\n      Text(\n          isCN\n              ? \"ProxyPin 可以动态解密 HTTPS 流量以展示原始请求/响应。\"\n              : \"ProxyPin can decrypt encrypted traffic on the fly and enable to see raw HTTPS requests and responses.\",\n          style: subtitleStyle,\n          textAlign: TextAlign.center),\n      const SizedBox(height: 45),\n    ];\n\n    if (isCertInstalled.value == false) {\n      children.add(const SizedBox(height: 20));\n      children.add(Icon(Icons.error_outline, color: Colors.red, size: 56));\n      children.add(const SizedBox(height: 12));\n      children.add(Text(localizations.certNotInstalled,\n          textAlign: TextAlign.center, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)));\n      children.add(const SizedBox(height: 20));\n      children.add(\n        FilledButton(\n            onPressed: () => _installCert(context),\n            style: FilledButton.styleFrom(\n                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),\n                padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 19)),\n            child: Text(localizations.install)),\n      );\n    } else if (isCertInstalled.value == true) {\n      children.add(Card(\n        elevation: 2,\n        color: Theme.brightnessOf(context) == Brightness.light ? Colors.grey[50] : Colors.grey[800],\n        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),\n        child: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 18.0),\n          child: Column(children: [\n            Icon(Icons.verified_rounded, color: Colors.green, size: 56),\n            const SizedBox(height: 12),\n            Text(isCN ? \"证书已安装\" : \"Certificate Installed\", style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),\n            const SizedBox(height: 8),\n            if (certDetails != null) ...[\n              const Divider(),\n              const SizedBox(height: 8),\n              // certificate details\n              Row(children: [\n                Text('Name', style: infoLabelStyle),\n                Expanded(\n                    child: SelectableText(certDetails!.subject['2.5.4.3'] ?? 'ProxyPin CA',\n                        style: infoValueStyle, textAlign: TextAlign.right)),\n              ]),\n              const SizedBox(height: 6),\n              Row(children: [\n                Text('Expires', style: infoLabelStyle),\n                Expanded(\n                    child: SelectableText(certDetails!.validity.notAfter.toLocal().toString().split(' ').first,\n                        style: infoValueStyle, textAlign: TextAlign.right)),\n              ]),\n              const SizedBox(height: 6),\n              Row(children: [\n                Text('Fingerprint', style: infoLabelStyle),\n                Expanded(\n                  child: SelectableText(certDetails!.sha1Thumbprint ?? '-',\n                      style: infoValueStyle, textAlign: TextAlign.right),\n                ),\n              ])\n            ]\n          ]),\n        ),\n      ));\n    }\n\n    return children;\n  }\n\n  void _installCert(BuildContext context) async {\n    final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n    var caFile = await CertificateManager.certificateFile();\n    bool success = await CertInstaller.installCertificate(caFile);\n    CertificateManager.cleanCache();\n\n    if (!context.mounted) {\n      return;\n    }\n\n    if (success) {\n      isCertInstalled.value = true;\n      CustomToast.success(isCN ? \"证书安装成功\" : \"Certificate installed successfully\").show(context);\n    } else {\n      isCertInstalled.value = false;\n      final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n      CustomToast.error(isCN ? \"证书安装失败，请尝试手动安装\" : \"Certificate installation failed, please try manual installation\")\n          .show(context);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/ssl/ssl.dart",
    "content": "import 'dart:io';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/util/crts.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/desktop/ssl/pc_cert.dart';\nimport 'package:proxypin/utils/ip.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass SslWidget extends StatefulWidget {\n  final ProxyServer proxyServer;\n\n  const SslWidget({super.key, required this.proxyServer});\n\n  @override\n  State<SslWidget> createState() => _SslState();\n}\n\nclass _SslState extends State<SslWidget> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return MenuAnchor(\n        builder: (context, controller, child) {\n          return IconButton(\n              icon: widget.proxyServer.enableSsl\n                  ? Icon(Icons.lock_open, size: 21)\n                  : Icon(Icons.https_outlined, color: Colors.red, size: 21),\n              tooltip: localizations.httpsProxy,\n              onPressed: () {\n                if (controller.isOpen) {\n                  controller.close();\n                } else {\n                  controller.open();\n                }\n              });\n        },\n        menuChildren: [\n          _Switch(proxyServer: widget.proxyServer, onEnableChange: (val) => setState(() {})),\n          item(localizations.installCaLocal,\n              onPressed: () => showDialog(context: context, builder: (context) => PCCert())),\n          item(\"${localizations.installRootCa} iOS\", onPressed: () async => iosCer(await localIp())),\n          item(\"${localizations.installRootCa} Android\", onPressed: () async => androidCer(await localIp())),\n          const Divider(thickness: 0.3, height: 3),\n          exportMenu(),\n          const Divider(thickness: 0.3, height: 3),\n          importMenu(),\n          const Divider(thickness: 0.3, height: 3),\n          item(localizations.generateCA, onPressed: () async {\n            showConfirmDialog(context, title: localizations.generateCA, content: localizations.generateCADescribe,\n                onConfirm: () async {\n              await CertificateManager.generateNewRootCA();\n              if (context.mounted) FlutterToastr.show(localizations.success, context);\n            });\n          }),\n          const Divider(thickness: 0.3, height: 3),\n          item(localizations.resetDefaultCA, onPressed: () async {\n            showConfirmDialog(context,\n                title: localizations.resetDefaultCA,\n                content: localizations.resetDefaultCADescribe, onConfirm: () async {\n              await CertificateManager.resetDefaultRootCA();\n              if (context.mounted) FlutterToastr.show(localizations.success, context);\n            });\n          }),\n        ]);\n  }\n\n  //import method\n  Widget importMenu() {\n    return item(localizations.importCaP12, onPressed: () async {\n      FilePickerResult? result =\n          await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['p12', 'pfx']);\n      if (result == null || !mounted) return;\n\n      //entry password\n      showDialog(\n          context: context,\n          builder: (BuildContext context) {\n            String? password;\n            return SimpleDialog(\n                title: Text(localizations.importCaP12, style: const TextStyle(fontSize: 16)),\n                children: [\n                  Padding(\n                    padding: const EdgeInsets.all(10),\n                    child: TextField(\n                      decoration: const InputDecoration(\n                        hintText: \"Enter the password of the p12 file\",\n                        border: OutlineInputBorder(),\n                      ),\n                      onChanged: (val) => password = val,\n                    ),\n                  ),\n                  Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n                    TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n                    TextButton(\n                      onPressed: () async {\n                        var file = File(result.files.single.path!);\n                        var bytes = await file.readAsBytes();\n                        try {\n                          await CertificateManager.importPkcs12(bytes, password);\n                          if (context.mounted) {\n                            FlutterToastr.show(localizations.success, context);\n                            Navigator.pop(context);\n                          }\n                        } catch (e, stackTrace) {\n                          logger.e('import p12 error [$password]', error: e, stackTrace: stackTrace);\n                          if (context.mounted) FlutterToastr.show(localizations.importFailed, context);\n                          return;\n                        }\n                      },\n                      child: Text(localizations.import),\n                    )\n                  ])\n                ]);\n          });\n    });\n  }\n\n  Widget exportMenu() {\n    return SubmenuButton(\n        menuChildren: [\n          MenuItemButton(\n              child: Padding(\n                  padding: const EdgeInsets.only(left: 10, right: 10),\n                  child: Text(localizations.exportCA, style: const TextStyle(fontSize: 14))),\n              onPressed: () async {\n                String? path = (await FilePicker.platform.saveFile(fileName: \"ProxyPinCA.crt\"));\n                if (path == null) return;\n\n                var caFile = await CertificateManager.certificateFile();\n                await caFile.copy(path);\n              }),\n          const Divider(thickness: 0.3, height: 8),\n          MenuItemButton(\n              child: Padding(\n                  padding: const EdgeInsets.only(left: 10, right: 10),\n                  child: Text(localizations.exportCaP12, style: const TextStyle(fontSize: 14))),\n              onPressed: () async {\n                //show p12 password\n                String? password;\n                showDialog(\n                    context: context,\n                    builder: (BuildContext context) {\n                      return SimpleDialog(\n                          title: Text(localizations.exportCaP12, style: const TextStyle(fontSize: 16)),\n                          children: [\n                            Padding(\n                              padding: const EdgeInsets.all(10),\n                              child: TextField(\n                                decoration: const InputDecoration(\n                                  hintStyle: TextStyle(color: Colors.grey),\n                                  hintText: \"Enter a password to protect p12 file\",\n                                  border: OutlineInputBorder(),\n                                ),\n                                onChanged: (val) => password = val,\n                              ),\n                            ),\n                            Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n                              TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n                              TextButton(\n                                onPressed: () async {\n                                  String? path = (await FilePicker.platform.saveFile(fileName: \"ProxyPinPkcs12.p12\"));\n                                  if (path == null) return;\n\n                                  var p12Bytes = await CertificateManager.generatePkcs12(\n                                      password?.isNotEmpty == true ? password : null);\n                                  await File(path).writeAsBytes(p12Bytes);\n                                  if (context.mounted) Navigator.pop(context);\n                                },\n                                child: Text(localizations.export),\n                              )\n                            ])\n                          ]);\n                    });\n              }),\n          MenuItemButton(\n              child: Padding(\n                  padding: const EdgeInsets.only(left: 10, right: 10),\n                  child: Text(localizations.exportPrivateKey, style: const TextStyle(fontSize: 14))),\n              onPressed: () async {\n                String? path = (await FilePicker.platform.saveFile(fileName: \"ProxyPinKey.pem\"));\n                if (path == null) return;\n\n                var keyFile = await CertificateManager.privateKeyFile();\n                await keyFile.copy(path);\n              }),\n        ],\n        child: Padding(\n            padding: const EdgeInsets.only(left: 10),\n            child: Text(localizations.export, style: const TextStyle(fontSize: 14))));\n  }\n\n  Widget item(String text, {VoidCallback? onPressed}) {\n    return MenuItemButton(\n        onPressed: onPressed,\n        child: Padding(\n            padding: const EdgeInsets.only(left: 10, right: 5),\n            child: Text(text, style: const TextStyle(fontSize: 14))));\n  }\n\n  void iosCer(String host) {\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (BuildContext context) {\n          return SimpleDialog(\n              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),\n              contentPadding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),\n              title: Row(children: [\n                const Expanded(child: SizedBox()),\n                Text(\"iOS ${localizations.caInstallGuide}\",\n                    style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n                const Expanded(child: SizedBox()),\n                Align(alignment: Alignment.topRight, child: CloseButton())\n              ]),\n              alignment: Alignment.center,\n              children: [\n                SelectableText.rich(\n                    TextSpan(text: \"1. ${localizations.configWifiProxy} Host：$host  Port：${widget.proxyServer.port}\")),\n                const SizedBox(height: 10),\n                Row(\n                  children: [\n                    Text(\"2. ${localizations.caIosBrowser}\\t\"),\n                    const SelectableText.rich(\n                        TextSpan(text: \"http://proxy.pin/ssl\", style: TextStyle(decoration: TextDecoration.underline)))\n                  ],\n                ),\n                const SizedBox(height: 10),\n                Text(\"3. ${localizations.installRootCa} -> ${localizations.trustCa}\"),\n                const SizedBox(height: 10),\n                Row(children: [\n                  Column(children: [\n                    Text(\"3.1 ${localizations.installCaDescribe}\", style: const TextStyle(fontSize: 12)),\n                    const SizedBox(height: 10),\n                    Image.network(\"https://foruda.gitee.com/images/1689346516243774963/c56bc546_1073801.png\",\n                        height: 270, width: 300)\n                  ]),\n                  const SizedBox(width: 10),\n                  Column(children: [\n                    Text(\"3.2 ${localizations.trustCaDescribe}\", style: const TextStyle(fontSize: 12)),\n                    const SizedBox(height: 10),\n                    Image.network(\"https://foruda.gitee.com/images/1689346614916658100/fd9b9e41_1073801.png\",\n                        height: 270, width: 300)\n                  ])\n                ])\n              ]);\n        });\n  }\n\n  void androidCer(String host) {\n    bool isCN = localizations.localeName == 'zh';\n\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (BuildContext context) {\n          return AlertDialog(\n              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),\n              contentPadding: const EdgeInsets.all(5),\n              title: Row(children: [\n                const Expanded(child: SizedBox()),\n                Text(\"Android ${localizations.caInstallGuide}\",\n                    style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n                const Expanded(child: SizedBox()),\n                Align(alignment: Alignment.topRight, child: CloseButton())\n              ]),\n              content: SizedBox(\n                  width: 600,\n                  child: DefaultTabController(\n                      length: 2,\n                      child: Scaffold(\n                        appBar: TabBar(tabs: <Widget>[\n                          Tab(text: localizations.androidRoot),\n                          Tab(text: localizations.androidUserCA),\n                        ]),\n                        body: Padding(\n                            padding: const EdgeInsets.all(10),\n                            child: TabBarView(children: [\n                              ListView(children: [\n                                Text(localizations.androidRootMagisk),\n                                TextButton(\n                                    child: Text(\n                                        \"https://${isCN ? 'gitee' : 'github'}.com/wanghongenpin/Magisk-ProxyPinCA/releases\"),\n                                    onPressed: () {\n                                      launchUrl(Uri.parse(\n                                          \"https://${isCN ? 'gitee' : 'github'}.com/wanghongenpin/Magisk-ProxyPinCA/releases\"));\n                                    }),\n                                const SizedBox(height: 10),\n                                futureWidget(CertificateManager.systemCertificateName(),\n                                    (name) => SelectableText(localizations.androidRootRename(name))),\n                                const SizedBox(height: 10),\n                                ClipRRect(\n                                    child: Align(\n                                        alignment: Alignment.topCenter,\n                                        child: Image.network(\n                                          scale: 0.5,\n                                          \"https://foruda.gitee.com/images/1710181660282752846/cb520c0b_1073801.png\",\n                                          height: 460,\n                                        )))\n                              ]),\n                              ListView(\n                                children: [\n                                  Text(localizations.androidUserCATips,\n                                      style: const TextStyle(fontWeight: FontWeight.w500)),\n                                  const SizedBox(height: 10),\n                                  SelectableText.rich(TextSpan(\n                                      text:\n                                          \"1. ${localizations.configWifiProxy} Host：$host  Port：${widget.proxyServer.port}\")),\n                                  const SizedBox(height: 10),\n                                  Row(\n                                    children: [\n                                      Text(\"2. ${localizations.caAndroidBrowser}\\t\"),\n                                      const SelectableText.rich(TextSpan(\n                                          text: \"http://proxy.pin/ssl\",\n                                          style: TextStyle(decoration: TextDecoration.underline)))\n                                    ],\n                                  ),\n                                  const SizedBox(height: 10),\n                                  Text(\"3. ${localizations.androidUserCAInstall}\"),\n                                  const SizedBox(height: 10),\n                                  TextButton(\n                                      onPressed: () {\n                                        launchUrl(Uri.parse(isCN\n                                            ? \"https://gitee.com/wanghongenpin/proxypin/wikis/%E5%AE%89%E5%8D%93%E6%97%A0ROOT%E4%BD%BF%E7%94%A8Xposed%E6%A8%A1%E5%9D%97%E6%8A%93%E5%8C%85\"\n                                            : \"https://github.com/wanghongenpin/proxypin/wiki/Android-without-ROOT-uses-Xposed-module-to-capture-packets\"));\n                                      },\n                                      child: Text(\" ${localizations.androidUserXposed}\")),\n                                  const SizedBox(height: 10),\n                                  ClipRRect(\n                                      child: Align(\n                                          alignment: Alignment.topCenter,\n                                          heightFactor: .7,\n                                          child: Image.network(\n                                            \"https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png\",\n                                            height: 530,\n                                          )))\n                                ],\n                              ),\n                            ])),\n                      ))));\n        });\n  }\n}\n\nclass _Switch extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final Function(bool val) onEnableChange;\n\n  const _Switch({required this.proxyServer, required this.onEnableChange});\n\n  @override\n  State<_Switch> createState() => _SwitchState();\n}\n\nclass _SwitchState extends State<_Switch> {\n  bool changed = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return MenuItemButton(\n        onPressed: () {},\n        child: Row(children: [\n          Padding(\n              padding: const EdgeInsets.only(left: 10, right: 5),\n              child: Text(localizations.enabledHttps, style: const TextStyle(fontSize: 14))),\n          Transform.scale(\n              scale: 0.8,\n              child: Switch(\n                  hoverColor: Colors.transparent,\n                  value: widget.proxyServer.enableSsl,\n                  onChanged: (val) {\n                    widget.proxyServer.enableSsl = val;\n                    changed = true;\n                    widget.onEnableChange(val);\n                    CertificateManager.cleanCache();\n                    setState(() {});\n                  }))\n        ]));\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    if (changed) {\n      widget.proxyServer.configuration.flushConfig();\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/toolbar/phone_connect.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:qr_flutter/qr_flutter.dart';\n\n/// @author wanghongen\n/// 2023/10/8\nclass PhoneConnect extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final List<String> hosts;\n\n  const PhoneConnect({super.key, required this.proxyServer, required this.hosts});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _PhoneConnectState();\n  }\n}\n\nclass _PhoneConnectState extends State<PhoneConnect> {\n  late String host;\n  late int port;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    host = widget.hosts.first;\n    port = widget.proxyServer.port;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),\n        title: Row(children: [\n          Text(localizations.mobileConnect, style: const TextStyle(fontSize: 18)),\n          const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))\n        ]),\n        contentPadding: const EdgeInsets.all(10),\n        content: SizedBox(\n            height: 300,\n            width: 300,\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                if (widget.proxyServer.isRunning)\n                  QrImageView(\n                      backgroundColor: Colors.white,\n                      data: \"proxypin://connect?host=$host&port=${widget.proxyServer.port}\",\n                      size: 200.0)\n                else\n                  SizedBox(\n                      height: 200,\n                      child: Center(child: Text(localizations.serverNotStart, style: const TextStyle(fontSize: 16)))),\n                const SizedBox(height: 10),\n                Row(mainAxisAlignment: MainAxisAlignment.center, children: [\n                  Text(localizations.localIP),\n                  DropdownButton(\n                      value: host,\n                      isDense: true,\n                      borderRadius: BorderRadius.circular(8),\n                      padding: const EdgeInsets.only(right: 10),\n                      items: widget.hosts\n                          .map((it) => DropdownMenuItem(\n                                value: it,\n                                child: SelectableText('$it:$port'),\n                              ))\n                          .toList(),\n                      onChanged: (String? value) {\n                        setState(() {\n                          host = value!;\n                        });\n                      })\n                ]),\n                const SizedBox(height: 10),\n                Text(localizations.mobileScan, style: const TextStyle(fontSize: 16)),\n              ],\n            )));\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/toolbar/toolbar.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/ui/desktop/toolbar/phone_connect.dart';\nimport 'package:proxypin/ui/desktop/setting/setting.dart';\nimport 'package:proxypin/ui/desktop/ssl/ssl.dart';\nimport 'package:proxypin/ui/launch/launch.dart';\nimport 'package:proxypin/utils/ip.dart';\nimport 'package:window_manager/window_manager.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\n\nimport '../request/list.dart';\n\n/// @author wanghongen\n/// 2023/10/8\nclass Toolbar extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final GlobalKey<DesktopRequestListState> requestListStateKey;\n\n  const Toolbar(this.proxyServer, this.requestListStateKey, {super.key});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _ToolbarState();\n  }\n}\n\nclass _ToolbarState extends State<Toolbar> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    HardwareKeyboard.instance.addHandler(onKeyEvent);\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if (HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.escape)) {\n      if (ModalRoute.of(context)?.isCurrent == false) {\n        Navigator.maybePop(context);\n        return true;\n      }\n    }\n\n    if (HardwareKeyboard.instance.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyW) {\n      windowManager.blur();\n      return true;\n    }\n\n    if (HardwareKeyboard.instance.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyQ) {\n      windowManager.close();\n      return true;\n    }\n\n    return false;\n  }\n\n  @override\n  void dispose() {\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(children: [\n      Padding(padding: EdgeInsets.only(left: Platform.isMacOS ? 83 : 20)),\n      SocketLaunch(proxyServer: widget.proxyServer, startup: widget.proxyServer.configuration.startup),\n      const Padding(padding: EdgeInsets.only(left: 18)),\n      IconButton(\n          tooltip: localizations.clear,\n          icon: const Icon(Icons.delete_outline, size: 21),\n          onPressed: () {\n            widget.requestListStateKey.currentState?.clean();\n          }),\n      const Padding(padding: EdgeInsets.only(left: 18)),\n      SslWidget(proxyServer: widget.proxyServer), // SSL配置\n      const Padding(padding: EdgeInsets.only(left: 18)),\n      Setting(proxyServer: widget.proxyServer), // 设置\n      const Padding(padding: EdgeInsets.only(left: 18)),\n      IconButton(\n          tooltip: localizations.mobileConnect,\n          icon: const Icon(Icons.phone_iphone_outlined, size: 21),\n          onPressed: () async {\n            final ips = await localIps(readCache: false);\n            phoneConnect(ips, widget.proxyServer.port);\n          }),\n      const Padding(padding: EdgeInsets.only(left: 10)),\n    ]);\n  }\n\n  void phoneConnect(List<String> hosts, int port) {\n    showDialog(\n        context: context,\n        builder: (context) {\n          return PhoneConnect(proxyServer: widget.proxyServer, hosts: hosts);\n        });\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/widgets/highlight.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:proxypin/ui/component/state_component.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/utils/keyword_highlight.dart';\n\n///配置关键词高亮\n///@Author: WangHongEn\nclass DesktopKeywordHighlight extends StatefulWidget {\n  const DesktopKeywordHighlight({super.key});\n\n  @override\n  State<DesktopKeywordHighlight> createState() => _KeywordHighlightState();\n}\n\nclass _KeywordHighlightState extends State<DesktopKeywordHighlight> {\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    var colors = {\n      Colors.red: localizations.red,\n      Colors.yellow.shade600: localizations.yellow,\n      Colors.blue: localizations.blue,\n      Colors.green: localizations.green,\n      Colors.grey: localizations.gray,\n    };\n\n    Map<Color, String> map = Map.of(KeywordHighlights.keywords);\n\n    return AlertDialog(\n      title: ListTile(\n          title: Text(localizations.keyword + localizations.highlight,\n              textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))),\n      titlePadding: const EdgeInsets.all(0),\n      actionsPadding: const EdgeInsets.only(right: 10, bottom: 10),\n      contentPadding: const EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 5),\n      actions: [\n        TextButton(\n          child: Text(localizations.cancel),\n          onPressed: () => Navigator.of(context).pop(),\n        ),\n        TextButton(\n          child: Text(localizations.done),\n          onPressed: () {\n            KeywordHighlights.saveKeywords(map);\n            Navigator.of(context).pop();\n          },\n        ),\n      ],\n      content: SizedBox(\n        height: 180,\n        width: 400,\n        child: DefaultTabController(\n          length: colors.length,\n          child: Scaffold(\n            appBar: TabBar(tabs: colors.entries.map((e) => Tab(text: e.value)).toList()),\n            body: TabBarView(\n                children: colors.entries\n                    .map((e) => KeepAliveWrapper(\n                        child: Padding(\n                            padding: const EdgeInsets.all(15),\n                            child: TextFormField(\n                              minLines: 2,\n                              maxLines: 2,\n                              initialValue: map[e.key],\n                              onChanged: (value) {\n                                if (value.isEmpty) {\n                                  map.remove(e.key);\n                                } else {\n                                  map[e.key] = value;\n                                }\n                              },\n                              decoration: decoration(localizations.keyword),\n                            ))))\n                    .toList()),\n          ),\n        ),\n      ),\n    );\n  }\n\n  InputDecoration decoration(String label, {String? hintText}) {\n    return InputDecoration(\n      floatingLabelBehavior: FloatingLabelBehavior.always,\n      labelText: label,\n      isDense: true,\n      border: const OutlineInputBorder(),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/widgets/windows_toolbar.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:window_manager/window_manager.dart';\n\nclass WindowsToolbar extends StatefulWidget {\n  final Widget? title;\n\n  const WindowsToolbar({\n    super.key,\n    this.title,\n  });\n\n  @override\n  State<WindowsToolbar> createState() => _WindowsToolbarState();\n}\n\nclass _WindowsToolbarState extends State<WindowsToolbar> with WindowListener {\n  @override\n  void initState() {\n    windowManager.addListener(this);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    windowManager.removeListener(this);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        SizedBox(width: 7),\n        Padding(\n            padding: EdgeInsets.only(top: 2),\n            child: Center(\n                child: Image.asset(\n              'assets/icon_foreground.png',\n              width: 32,\n            ))),\n        widget.title ?? SizedBox(),\n        Expanded(child: DragToMoveArea(child: Container())),\n        WindowCaptionButton.minimize(\n            brightness: Theme.brightnessOf(context),\n            onPressed: () async {\n              bool isMinimized = await windowManager.isMinimized();\n              if (isMinimized) {\n                windowManager.restore();\n              } else {\n                windowManager.minimize();\n              }\n            }),\n        FutureBuilder<bool>(\n            future: windowManager.isMaximized(),\n            builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {\n              if (snapshot.data == true) {\n                return WindowCaptionButton.unmaximize(\n                  brightness: Theme.brightnessOf(context),\n                  onPressed: () {\n                    windowManager.unmaximize();\n                  },\n                );\n              }\n              return WindowCaptionButton.maximize(\n                brightness: Theme.brightnessOf(context),\n                onPressed: () {\n                  windowManager.maximize();\n                },\n              );\n            }),\n        WindowCaptionButton.close(\n            brightness: Theme.brightnessOf(context),\n            onPressed: () {\n              windowManager.close();\n            }),\n      ],\n    );\n  }\n\n  @override\n  void onWindowMaximize() {\n    setState(() {});\n  }\n\n  @override\n  void onWindowUnmaximize() {\n    setState(() {});\n  }\n}\n"
  },
  {
    "path": "lib/ui/desktop/window_listener.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:window_manager/window_manager.dart';\n\n/// 监听窗口变化\nclass WindowChangeListener extends WindowListener {\n  final AppConfiguration appConfiguration;\n\n  WindowChangeListener(this.appConfiguration);\n\n  @override\n  void onWindowResized() async {\n    final windowSize = await windowManager.getSize();\n    logger.d(\"windowSize: $windowSize\");\n    appConfiguration.windowSize = windowSize;\n    appConfiguration.flushConfig();\n  }\n\n  @override\n  void onWindowMoved() async {\n    final windowPosition = await windowManager.getPosition();\n    // logger.d(\"windowPosition: $windowPosition\");\n    appConfiguration.windowPosition = windowPosition;\n    appConfiguration.flushConfig();\n  }\n}\n"
  },
  {
    "path": "lib/ui/launch/launch.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:io';\nimport 'dart:ui';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/native/vpn.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/desktop/ssl/pc_cert.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:window_manager/window_manager.dart';\n\nimport '../mobile/setting/ssl.dart';\n\n///启动按钮\n///@author wanghongen\n///2023/10/8\nclass SocketLaunch extends StatefulWidget {\n  static ValueNotifier<ValueWrap<bool>> startStatus = ValueNotifier(ValueWrap());\n\n  final ProxyServer proxyServer;\n  final int size;\n  final bool startup; //默认是否启动\n  final Function? onStart;\n  final Function? onStop;\n\n  final bool serverLaunch; //是否启动代理服务器\n\n  const SocketLaunch(\n      {super.key,\n      required this.proxyServer,\n      this.size = 25,\n      this.onStart,\n      this.onStop,\n      this.startup = true,\n      this.serverLaunch = true});\n\n  @override\n  State<StatefulWidget> createState() => _SocketLaunchState();\n}\n\nclass _SocketLaunchState extends State<SocketLaunch> with WindowListener, WidgetsBindingObserver {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n  bool started = false;\n\n  @override\n  void initState() {\n    super.initState();\n    if (Platforms.isDesktop()) {\n      windowManager.addListener(this);\n      windowManager.setPreventClose(true);\n    }\n\n    WidgetsBinding.instance.addObserver(this);\n    //启动代理服务器\n    if (widget.startup) {\n      start();\n    }\n\n    SocketLaunch.startStatus.addListener(() {\n      if (SocketLaunch.startStatus.value.get() == started) {\n        return;\n      }\n      setState(() {\n        started = SocketLaunch.startStatus.value.get() ?? started;\n      });\n    });\n  }\n\n  @override\n  void dispose() {\n    windowManager.removeListener(this);\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n\n  @override\n  void onWindowClose() async {\n    logger.d(\"onWindowClose\");\n    await appExit();\n  }\n\n  Future<void> appExit() async {\n    logger.d(\"appExit\");\n    await widget.proxyServer.stop();\n    started = false;\n    if (Platforms.isDesktop()) {\n      windowManager.setPreventClose(false);\n      await windowManager.destroy();\n    }\n\n    if (!Platform.isWindows && !Platform.isLinux) {\n      try {\n        await SystemNavigator.pop(animated: true).timeout(const Duration(milliseconds: 150));\n      } catch (_) {\n        //\n      }\n    }\n\n    exit(0);\n  }\n\n  @override\n  Future<AppExitResponse> didRequestAppExit() async {\n    await appExit();\n    return super.didRequestAppExit();\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    if (state == AppLifecycleState.resumed) {\n      if (widget.proxyServer.isRunning) {\n        widget.proxyServer.retryBind();\n      }\n\n      if (Platforms.isMobile() && started == false) {\n        Vpn.isRunning().then((value) {\n          Vpn.isVpnStarted = value;\n          SocketLaunch.startStatus.value = ValueWrap.of(value);\n        });\n      }\n    }\n\n    if (state == AppLifecycleState.detached) {\n      logger.d('AppLifecycleState.detached');\n      widget.onStop?.call();\n      widget.proxyServer.stop();\n      started = false;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Color primaryColor = Theme.of(context).colorScheme.primary;\n    return IconButton(\n        tooltip: started ? localizations.stop : localizations.start,\n        icon: Icon(started ? Icons.stop : Icons.play_arrow_sharp,\n            color: started ? Colors.red : primaryColor, size: widget.size.toDouble()),\n        onPressed: () async {\n          if (started) {\n            if (!widget.serverLaunch) {\n              setState(() {\n                widget.onStop?.call();\n                started = !started;\n              });\n              return;\n            }\n\n            widget.proxyServer.stop().then((value) {\n              widget.onStop?.call();\n              setState(() {\n                started = !started;\n              });\n            });\n          } else {\n            start();\n          }\n        });\n  }\n\n  ///启动代理服务器\n  Future<void> start() async {\n    try {\n      if (!widget.serverLaunch) {\n        await widget.onStart?.call();\n        setState(() {\n          started = true;\n        });\n        return;\n      }\n\n      widget.proxyServer.start().then((value) {\n        setState(() {\n          started = true;\n        });\n        widget.onStart?.call();\n      }).catchError((e) {\n        logger.e(\"启动代理服务器失败\", error: e);\n        String message = localizations.proxyPortRepeat(widget.proxyServer.port);\n        FlutterToastr.show(message, context, duration: 3);\n      });\n    } finally {\n      Future.delayed(const Duration(seconds: 5)).then((value) {\n        if (!mounted) {\n          return;\n        }\n        if (Platforms.isDesktop()) {\n          PCCertChecker.check(context);\n        } else if (Platform.isIOS) {\n          IOSCertChecker.check(context);\n        }\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/debug/breakpoint_executor.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/components/request_breakpoint.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/ui/mobile/request/request_editor.dart';\nimport 'package:proxypin/ui/mobile/request/request_editor_source.dart';\n\nclass BreakpointExecutor extends StatefulWidget {\n  final HttpRequest request;\n  final HttpResponse? response;\n  final String requestId;\n\n  // false: intercept request, true: intercept response\n  final bool isResponse;\n\n  const BreakpointExecutor({\n    super.key,\n    required this.request,\n    this.response,\n    required this.requestId,\n    required this.isResponse,\n  });\n\n  @override\n  State<BreakpointExecutor> createState() => _BreakpointExecutorState();\n}\n\nclass _BreakpointExecutorState extends State<BreakpointExecutor> {\n  late HttpRequest request;\n  late HttpResponse? response;\n\n  @override\n  void initState() {\n    super.initState();\n    request = widget.request;\n    response = widget.response;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.isResponse) {\n      return _buildResponseBody();\n    }\n\n    return MobileRequestEditor(\n      request: request,\n      proxyServer: ProxyServer.current,\n      source: RequestEditorSource.breakpointRequest,\n      onExecuteRequest: (newRequest) async {\n        if (Navigator.canPop(context)) {\n          Navigator.pop(context, newRequest);\n          RequestBreakpointInterceptor.instance.resumeRequest(widget.requestId, newRequest);\n        }\n      },\n    );\n  }\n\n  Widget _buildResponseBody() {\n    return MobileRequestEditor(\n      request: request,\n      response: response,\n      proxyServer: ProxyServer.current,\n      source: RequestEditorSource.breakpointResponse,\n      onExecuteResponse: (newResponse) async {\n        if (Navigator.canPop(context)) {\n          Navigator.pop(context, newResponse);\n          RequestBreakpointInterceptor.instance.resumeResponse(widget.requestId, newResponse);\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/menu/bottom_navigation.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/components/manager/hosts_manager.dart';\nimport 'package:proxypin/network/components/manager/request_block_manager.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/util/system_proxy.dart';\nimport 'package:proxypin/storage/histories.dart';\nimport 'package:proxypin/ui/component/proxy_port_setting.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/mobile/menu/drawer.dart';\nimport 'package:proxypin/ui/mobile/setting/hosts.dart';\nimport 'package:proxypin/ui/mobile/setting/preference.dart';\nimport 'package:proxypin/ui/mobile/mobile.dart';\nimport 'package:proxypin/ui/mobile/request/favorite.dart';\nimport 'package:proxypin/ui/mobile/request/history.dart';\nimport 'package:proxypin/ui/mobile/setting/request_block.dart';\nimport 'package:proxypin/ui/mobile/setting/request_crypto.dart';\nimport 'package:proxypin/ui/mobile/setting/request_rewrite.dart';\nimport 'package:proxypin/ui/mobile/setting/script.dart';\nimport 'package:proxypin/ui/mobile/setting/ssl.dart';\nimport 'package:proxypin/ui/mobile/widgets/about.dart';\nimport 'package:proxypin/ui/mobile/setting/request_breakpoint.dart';\n\nimport '../../../network/components/manager/request_breakpoint_manager.dart';\nimport '../../component/widgets.dart';\nimport '../setting/proxy.dart';\nimport '../setting/request_map.dart';\n\n/// @author wanghongen\n/// 2024/9/30\nclass ConfigPage extends StatefulWidget {\n  final ProxyServer proxyServer;\n\n  const ConfigPage({super.key, required this.proxyServer});\n\n  @override\n  State<StatefulWidget> createState() => _ConfigPageState();\n}\n\nclass _ConfigPageState extends State<ConfigPage> {\n  late ProxyServer proxyServer = widget.proxyServer;\n  late HistoryTask historyTask;\n\n  @override\n  void initState() {\n    super.initState();\n    historyTask = HistoryTask.ensureInstance(proxyServer.configuration, MobileApp.container);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    Color color = Theme.of(context).colorScheme.primary.withValues(alpha: 0.85);\n\n    Widget section(List<Widget> tiles) => Card(\n          color: Colors.transparent,\n          elevation: 0,\n          shape: RoundedRectangleBorder(\n              side: BorderSide(color: Theme.of(context).dividerColor.withValues(alpha: 0.13)),\n              borderRadius: BorderRadius.circular(10)),\n          child: Column(children: tiles),\n        );\n\n    Widget arrow = const Icon(Icons.arrow_forward_ios, size: 16);\n\n    return Scaffold(\n        appBar: PreferredSize(\n            preferredSize: const Size.fromHeight(42),\n            child: AppBar(\n              title: Text(localizations.config, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w400)),\n              centerTitle: true,\n            )),\n        body: ListView(\n          padding: const EdgeInsets.all(12),\n          children: [\n            section([\n              ListTile(\n                  leading: Icon(Icons.favorite_outline, color: color),\n                  title: Text(localizations.favorites),\n                  trailing: arrow,\n                  onTap: () => navigator(context, MobileFavorites(proxyServer: proxyServer))),\n              Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n              ListTile(\n                leading: Icon(Icons.history, color: color),\n                title: Text(localizations.history),\n                trailing: arrow,\n                onTap: () => navigator(context,\n                    MobileHistory(proxyServer: proxyServer, container: MobileApp.container, historyTask: historyTask)),\n              ),\n            ]),\n            const SizedBox(height: 12),\n            section([\n              ListTile(\n                  title: Text(localizations.hosts),\n                  leading: Icon(Icons.domain, color: color),\n                  trailing: arrow,\n                  onTap: () async {\n                    var hostsManager = await HostsManager.instance;\n                    if (context.mounted) {\n                      navigator(context, HostsPage(hostsManager: hostsManager));\n                    }\n                  }),\n              Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n              ListTile(\n                  title: Text(localizations.requestBlock),\n                  leading: Icon(Icons.block_flipped, color: color),\n                  trailing: arrow,\n                  onTap: () async {\n                    var requestBlockManager = await RequestBlockManager.instance;\n                    if (context.mounted) {\n                      navigator(context, MobileRequestBlock(requestBlockManager: requestBlockManager));\n                    }\n                  }),\n              Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n              ListTile(\n                  title: Text(localizations.requestRewrite),\n                  leading: Icon(Icons.edit_outlined, color: color),\n                  trailing: arrow,\n                  onTap: () async {\n                    var requestRewrites = await RequestRewriteManager.instance;\n                    if (context.mounted) {\n                      navigator(context, MobileRequestRewrite(requestRewrites: requestRewrites));\n                    }\n                  }),\n              Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n              ListTile(\n                  title: Text(localizations.requestMap),\n                  leading: Icon(Icons.swap_horiz_outlined, color: color),\n                  trailing: arrow,\n                  onTap: () => navigator(context, MobileRequestMapPage())),\n              Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n              ListTile(\n                  title: Text(localizations.requestCrypto),\n                  leading: Icon(Icons.lock_outline, color: color),\n                  trailing: arrow,\n                  onTap: () => navigator(context, const MobileRequestCryptoPage())),\n              Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n              ListTile(\n                  title: Text(localizations.script),\n                  leading: Icon(Icons.javascript_outlined, color: color),\n                  trailing: arrow,\n                  onTap: () => navigator(context, const MobileScript())),\n              Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n              ListTile(\n                  title: Text(localizations.breakpoint),\n                  leading: Icon(Icons.bug_report_outlined, color: color),\n                  trailing: arrow,\n                  onTap: () async {\n                    var manager = await RequestBreakpointManager.instance;\n                    if (context.mounted) {\n                      navigator(context, MobileRequestBreakpointPage(manager: manager));\n                    }\n                  })\n            ]),\n            const SizedBox(height: 16)\n          ],\n        ));\n  }\n}\n\nvoid navigator(BuildContext context, Widget widget) async {\n  if (context.mounted) {\n    Navigator.of(context).push(\n      MaterialPageRoute(builder: (BuildContext context) => widget),\n    );\n  }\n}\n\nclass SettingPage extends StatelessWidget {\n  final ProxyServer proxyServer;\n  final AppConfiguration appConfiguration;\n\n  const SettingPage({super.key, required this.proxyServer, required this.appConfiguration});\n\n  @override\n  Widget build(BuildContext context) {\n    final configuration = proxyServer.configuration;\n\n    var textEditingController = TextEditingController(text: configuration.proxyPassDomains);\n\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    bool isEn = appConfiguration.language?.languageCode == 'en';\n\n    Widget section(List<Widget> tiles) => Card(\n          color: Colors.transparent,\n          elevation: 0,\n          shape: RoundedRectangleBorder(\n              side: BorderSide(color: Theme.of(context).dividerColor.withValues(alpha: 0.13)),\n              borderRadius: BorderRadius.circular(10)),\n          child: Column(children: tiles),\n        );\n\n    return Scaffold(\n        appBar: PreferredSize(\n            preferredSize: const Size.fromHeight(42),\n            child: AppBar(\n              title: Text(localizations.setting, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w400)),\n              centerTitle: true,\n            )),\n        body: ListView(padding: const EdgeInsets.all(12), children: [\n          section([\n            ListTile(\n                title: Text(localizations.httpsProxy),\n                trailing: const Icon(Icons.keyboard_arrow_right),\n                onTap: () => navigator(context, MobileSslWidget(proxyServer: proxyServer))),\n            Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n            ListTile(\n                title: Text(localizations.filter),\n                trailing: const Icon(Icons.keyboard_arrow_right),\n                onTap: () => navigator(context, FilterMenu(proxyServer: proxyServer))),\n          ]),\n          const SizedBox(height: 12),\n          // Port and switches\n          Card(\n              color: Colors.transparent,\n              elevation: 0,\n              shape: RoundedRectangleBorder(\n                  side: BorderSide(color: Theme.of(context).dividerColor.withValues(alpha: 0.13)),\n                  borderRadius: BorderRadius.circular(10)),\n              child: Column(children: [\n                PortWidget(\n                    proxyServer: proxyServer,\n                    title: '${localizations.proxy}${isEn ? ' ' : ''}${localizations.port}',\n                    textStyle: const TextStyle(fontSize: 16)),\n                Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                if (Platform.isAndroid)\n                  ListTile(\n                      title: Text(localizations.systemProxy),\n                      trailing: SwitchWidget(\n                          value: configuration.enableSystemProxy,\n                          scale: 0.8,\n                          onChanged: (value) {\n                            configuration.enableSystemProxy = value;\n                            proxyServer.configuration.flushConfig();\n                          })),\n                if (Platform.isAndroid)\n                  Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                ListTile(\n                    title: const Text(\"SOCKS5\"),\n                    trailing: SwitchWidget(\n                        value: configuration.enableSocks5,\n                        scale: 0.8,\n                        onChanged: (value) {\n                          configuration.enableSocks5 = value;\n                          proxyServer.configuration.flushConfig();\n                        })),\n                Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                ListTile(\n                    title: Text(localizations.enabledHTTP2),\n                    trailing: SwitchWidget(\n                        value: configuration.enabledHttp2,\n                        scale: 0.8,\n                        onChanged: (value) {\n                          configuration.enabledHttp2 = value;\n                          proxyServer.configuration.flushConfig();\n                        })),\n                Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                ListTile(\n                    title: Text(localizations.externalProxy),\n                    trailing: const Icon(Icons.keyboard_arrow_right),\n                    onTap: () {\n                      showDialog(\n                          context: context,\n                          builder: (_) => ExternalProxyDialog(configuration: proxyServer.configuration));\n                    }),\n                Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n\n                Padding(\n                    padding: const EdgeInsets.only(left: 15),\n                    child: Row(children: [\n                      Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          Text(localizations.proxyIgnoreDomain, style: const TextStyle(fontSize: 14)),\n                          const SizedBox(height: 3),\n                          Text(isEn ? \"Use ';' to separate multiple entries\" : \"多个使用;分割\",\n                              style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),\n                        ],\n                      ),\n                      Padding(\n                          padding: const EdgeInsets.only(left: 35),\n                          child: TextButton(\n                            child: Text(localizations.reset),\n                            onPressed: () {\n                              textEditingController.text = SystemProxy.proxyPassDomains;\n                            },\n                          ))\n                    ])),\n                const SizedBox(height: 5),\n                Padding(\n                    padding: const EdgeInsets.only(left: 15, right: 5),\n                    child: TextField(\n                        textInputAction: TextInputAction.done,\n                        style: const TextStyle(fontSize: 13),\n                        controller: textEditingController,\n                        onSubmitted: (_) {\n                          configuration.proxyPassDomains = textEditingController.text;\n                          proxyServer.configuration.flushConfig();\n                        },\n                        decoration: const InputDecoration(\n                          contentPadding: EdgeInsets.all(10),\n                          border: OutlineInputBorder(),\n                        ),\n                        maxLines: 5,\n                        minLines: 1)),\n                // const SizedBox(height: 10),\n              ])),\n          const SizedBox(height: 12),\n          section([\n            ListTile(\n                title: Text(localizations.setting),\n                trailing: const Icon(Icons.keyboard_arrow_right),\n                onTap: () =>\n                    navigator(context, Preference(proxyServer: proxyServer, appConfiguration: appConfiguration))),\n            Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n            ListTile(\n                title: Text(localizations.about),\n                trailing: const Icon(Icons.keyboard_arrow_right),\n                onTap: () => navigator(context, const About())),\n          ]),\n          const SizedBox(height: 8),\n        ]));\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/menu/drawer.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/components/host_filter.dart';\nimport 'package:proxypin/network/components/manager/hosts_manager.dart';\nimport 'package:proxypin/network/components/manager/request_block_manager.dart';\nimport 'package:proxypin/network/components/manager/request_breakpoint_manager.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/system_proxy.dart';\nimport 'package:proxypin/storage/histories.dart';\nimport 'package:proxypin/ui/mobile/setting/hosts.dart';\nimport 'package:proxypin/ui/mobile/setting/request_breakpoint.dart';\nimport 'package:proxypin/ui/mobile/setting/request_map.dart';\nimport 'package:proxypin/ui/toolbox/toolbox.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/mobile/setting/preference.dart';\nimport 'package:proxypin/ui/mobile/request/favorite.dart';\nimport 'package:proxypin/ui/mobile/request/history.dart';\nimport 'package:proxypin/ui/mobile/setting/app_filter.dart';\nimport 'package:proxypin/ui/mobile/setting/filter.dart';\nimport 'package:proxypin/ui/mobile/setting/request_block.dart';\nimport 'package:proxypin/ui/mobile/setting/request_rewrite.dart';\nimport 'package:proxypin/ui/mobile/setting/request_crypto.dart';\nimport 'package:proxypin/ui/mobile/setting/script.dart';\nimport 'package:proxypin/ui/mobile/setting/ssl.dart';\nimport 'package:proxypin/ui/mobile/widgets/about.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\n\nimport '../../component/proxy_port_setting.dart';\nimport '../../component/widgets.dart';\nimport '../../desktop/setting/external_proxy.dart';\n\n///左侧抽屉\nclass DrawerWidget extends StatelessWidget {\n  final ProxyServer proxyServer;\n  final ListenableList<HttpRequest> container;\n  final HistoryTask historyTask;\n\n  DrawerWidget({super.key, required this.proxyServer, required this.container})\n      : historyTask = HistoryTask.ensureInstance(proxyServer.configuration, container);\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return Drawer(\n        backgroundColor: Theme.of(context).cardColor,\n        child: ListView(\n          padding: EdgeInsets.zero,\n          children: [\n            DrawerHeader(\n                decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer),\n                child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [\n                  CircleAvatar(\n                      radius: 24,\n                      backgroundColor: Colors.white,\n                      child: Center(\n                          child: Image.asset(\n                        'assets/icon_foreground.png',\n                        width: 52,\n                      ))),\n                  const SizedBox(width: 12),\n                  Expanded(\n                    child: Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        mainAxisAlignment: MainAxisAlignment.center,\n                        children: [\n                          Text('ProxyPin', style: Theme.of(context).textTheme.titleLarge),\n                          const SizedBox(height: 4),\n                          Text(isCN ? \"全平台开源免费抓包软件\" : \"Full platform open source free capture HTTP(S) traffic software\",\n                              maxLines: 2,\n                              overflow: TextOverflow.ellipsis,\n                              style: Theme.of(context).textTheme.bodySmall)\n                        ]),\n                  )\n                ])),\n            // Favorites & History\n            ListTile(\n                leading: const Icon(Icons.favorite),\n                title: Text(localizations.favorites),\n                onTap: () => navigator(context, MobileFavorites(proxyServer: proxyServer))),\n            ListTile(\n              leading: const Icon(Icons.history),\n              title: Text(localizations.history),\n              onTap: () => navigator(\n                  context, MobileHistory(proxyServer: proxyServer, container: container, historyTask: historyTask)),\n            ),\n            const Divider(thickness: 0.3, height: 0),\n            ListTile(\n                leading: const Icon(Icons.construction),\n                title: Text(localizations.toolbox),\n                onTap: () => Navigator.of(context).push(\n                      MaterialPageRoute(builder: (BuildContext context) {\n                        return Scaffold(\n                            appBar: AppBar(title: Text(localizations.toolbox), centerTitle: true),\n                            body: Toolbox(proxyServer: proxyServer));\n                      }),\n                    )),\n            ListTile(\n                title: Text(localizations.httpsProxy),\n                leading: proxyServer.enableSsl ? Icon(Icons.lock_open) : Icon(Icons.https),\n                onTap: () => navigator(context, MobileSslWidget(proxyServer: proxyServer))),\n            const Divider(thickness: 0.3, height: 0),\n            ListTile(\n                title: Text(localizations.filter),\n                leading: const Icon(Icons.filter_alt_outlined),\n                onTap: () => navigator(context, FilterMenu(proxyServer: proxyServer))),\n            ListTile(\n                title: Text(localizations.hosts),\n                leading: Icon(Icons.domain),\n                onTap: () async {\n                  var hostsManager = await HostsManager.instance;\n                  if (context.mounted) {\n                    navigator(context, HostsPage(hostsManager: hostsManager));\n                  }\n                }),\n            ListTile(\n                title: Text(localizations.requestBlock),\n                leading: const Icon(Icons.block_flipped),\n                onTap: () async {\n                  var requestBlockManager = await RequestBlockManager.instance;\n                  if (context.mounted) {\n                    navigator(context, MobileRequestBlock(requestBlockManager: requestBlockManager));\n                  }\n                }),\n            ListTile(\n                title: Text(localizations.requestRewrite),\n                leading: const Icon(Icons.edit_outlined),\n                onTap: () async {\n                  var requestRewrites = await RequestRewriteManager.instance;\n                  if (context.mounted) {\n                    navigator(context, MobileRequestRewrite(requestRewrites: requestRewrites));\n                  }\n                }),\n            ListTile(\n                title: Text(localizations.requestMap),\n                leading: Icon(Icons.swap_horiz_outlined),\n                onTap: () => navigator(context, MobileRequestMapPage())),\n            ListTile(\n                title: Text(localizations.requestCrypto),\n                leading: const Icon(Icons.lock_outline),\n                onTap: () => navigator(context, const MobileRequestCryptoPage())),\n            ListTile(\n                title: Text(localizations.script),\n                leading: const Icon(Icons.code),\n                onTap: () => navigator(context, const MobileScript())),\n            ListTile(\n                title: Text(localizations.breakpoint),\n                leading: const Icon(Icons.bug_report_outlined),\n                onTap: () async {\n                  var manager = await RequestBreakpointManager.instance;\n                  if (context.mounted) {\n                    navigator(context, MobileRequestBreakpointPage(manager: manager));\n                  }\n                }),\n            ListTile(\n                title: Text(localizations.setting),\n                leading: const Icon(Icons.settings),\n                onTap: () => navigator(\n                    context,\n                    futureWidget(\n                        AppConfiguration.instance,\n                        (appConfiguration) =>\n                            _SettingPage(proxyServer: proxyServer, appConfiguration: appConfiguration)))),\n            ListTile(\n                title: Text(localizations.about),\n                leading: const Icon(Icons.info_outline),\n                onTap: () => navigator(context, const About())),\n            const SizedBox(height: 20)\n          ],\n        ));\n  }\n}\n\n///跳转页面\nvoid navigator(BuildContext context, Widget widget) {\n  Navigator.of(context).push(\n    MaterialPageRoute(builder: (BuildContext context) {\n      return widget;\n    }),\n  );\n}\n\nclass _SettingPage extends StatelessWidget {\n  final ProxyServer proxyServer;\n  final AppConfiguration appConfiguration;\n\n  const _SettingPage({required this.proxyServer, required this.appConfiguration});\n\n  @override\n  Widget build(BuildContext context) {\n    final configuration = proxyServer.configuration;\n    var textEditingController = TextEditingController(text: configuration.proxyPassDomains);\n\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    bool isEn = appConfiguration.language?.languageCode == 'en';\n\n    Widget section(List<Widget> tiles) => Card(\n          color: Colors.transparent,\n          elevation: 0,\n          shape: RoundedRectangleBorder(\n              side: BorderSide(color: Theme.of(context).dividerColor.withValues(alpha: 0.13)),\n              borderRadius: BorderRadius.circular(10)),\n          child: Column(children: tiles),\n        );\n\n    return Scaffold(\n        appBar: PreferredSize(\n            preferredSize: const Size.fromHeight(42),\n            child: AppBar(\n              title: Text(localizations.setting, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w400)),\n              centerTitle: true,\n            )),\n        body: ListView(padding: const EdgeInsets.all(12), children: [\n          // Port and switches\n          Card(\n              color: Colors.transparent,\n              elevation: 0,\n              shape: RoundedRectangleBorder(\n                  side: BorderSide(color: Theme.of(context).dividerColor.withValues(alpha: 0.13)),\n                  borderRadius: BorderRadius.circular(10)),\n              child: Column(children: [\n                PortWidget(\n                    proxyServer: proxyServer,\n                    title: '${localizations.proxy}${isEn ? ' ' : ''}${localizations.port}',\n                    textStyle: const TextStyle(fontSize: 16)),\n                Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                if (Platform.isAndroid)\n                  ListTile(\n                      title: Text(localizations.systemProxy),\n                      trailing: SwitchWidget(\n                          value: configuration.enableSystemProxy,\n                          scale: 0.8,\n                          onChanged: (value) {\n                            configuration.enableSystemProxy = value;\n                            proxyServer.configuration.flushConfig();\n                          })),\n                if (Platform.isAndroid)\n                  Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                ListTile(\n                    title: const Text(\"SOCKS5\"),\n                    trailing: SwitchWidget(\n                        value: configuration.enableSocks5,\n                        scale: 0.8,\n                        onChanged: (value) {\n                          configuration.enableSocks5 = value;\n                          proxyServer.configuration.flushConfig();\n                        })),\n                Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                ListTile(\n                    title: Text(localizations.enabledHTTP2),\n                    trailing: SwitchWidget(\n                        value: configuration.enabledHttp2,\n                        scale: 0.8,\n                        onChanged: (value) {\n                          configuration.enabledHttp2 = value;\n                          proxyServer.configuration.flushConfig();\n                        })),\n                Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                ListTile(\n                    title: Text(localizations.externalProxy),\n                    trailing: const Icon(Icons.keyboard_arrow_right),\n                    onTap: () {\n                      showDialog(\n                          context: context,\n                          builder: (_) => ExternalProxyDialog(configuration: proxyServer.configuration));\n                    }),\n                Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                Padding(\n                    padding: const EdgeInsets.only(left: 15),\n                    child: Row(children: [\n                      Expanded(\n                          child: Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          Text(localizations.proxyIgnoreDomain, style: const TextStyle(fontSize: 14)),\n                          const SizedBox(height: 3),\n                          Text(isEn ? \"Use ';' to separate multiple entries\" : \"多个使用;分割\",\n                              style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),\n                        ],\n                      )),\n                      Padding(\n                          padding: const EdgeInsets.only(left: 35),\n                          child: TextButton(\n                            child: Text(localizations.reset),\n                            onPressed: () {\n                              textEditingController.text = SystemProxy.proxyPassDomains;\n                            },\n                          ))\n                    ])),\n                const SizedBox(height: 5),\n                Padding(\n                    padding: const EdgeInsets.only(left: 15, right: 5),\n                    child: TextField(\n                        textInputAction: TextInputAction.done,\n                        style: const TextStyle(fontSize: 13),\n                        controller: textEditingController,\n                        onSubmitted: (_) {\n                          configuration.proxyPassDomains = textEditingController.text;\n                          proxyServer.configuration.flushConfig();\n                        },\n                        decoration:\n                            const InputDecoration(contentPadding: EdgeInsets.all(10), border: OutlineInputBorder()),\n                        maxLines: 5,\n                        minLines: 1)),\n                const SizedBox(height: 10),\n              ])),\n          const SizedBox(height: 12),\n          section([\n            ListTile(\n                title: Text(localizations.preference),\n                trailing: const Icon(Icons.keyboard_arrow_right),\n                onTap: () =>\n                    navigator(context, Preference(proxyServer: proxyServer, appConfiguration: appConfiguration))),\n            Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n            ListTile(\n                title: Text(localizations.about),\n                trailing: const Icon(Icons.keyboard_arrow_right),\n                onTap: () => navigator(context, const About())),\n          ]),\n          const SizedBox(height: 8),\n        ]));\n  }\n}\n\n///抓包过滤菜单\nclass FilterMenu extends StatelessWidget {\n  final ProxyServer proxyServer;\n\n  const FilterMenu({super.key, required this.proxyServer});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    return Scaffold(\n        appBar: AppBar(title: Text(localizations.filter, style: const TextStyle(fontSize: 16)), centerTitle: true),\n        body: Padding(\n            padding: const EdgeInsets.all(12),\n            child: Card(\n                color: Colors.transparent,\n                elevation: 0,\n                shape: RoundedRectangleBorder(\n                    side: BorderSide(color: Theme.of(context).dividerColor.withValues(alpha: 0.13)),\n                    borderRadius: BorderRadius.circular(10)),\n                child: Column(mainAxisSize: MainAxisSize.min, children: [\n                  ListTile(\n                      title: Text(localizations.domainWhitelist),\n                      trailing: const Icon(Icons.arrow_right),\n                      onTap: () => navigator(\n                          context,\n                          MobileFilterWidget(\n                              configuration: proxyServer.configuration, hostList: HostFilter.whitelist))),\n                  Divider(height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                  ListTile(\n                      title: Text(localizations.domainBlacklist),\n                      trailing: const Icon(Icons.arrow_right),\n                      onTap: () => navigator(\n                          context,\n                          MobileFilterWidget(\n                              configuration: proxyServer.configuration, hostList: HostFilter.blacklist))),\n                  Platform.isIOS\n                      ? const SizedBox()\n                      : Column(mainAxisSize: MainAxisSize.min, children: [\n                          Divider(\n                              height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                          ListTile(\n                              title: Text(localizations.appWhitelist),\n                              trailing: const Icon(Icons.arrow_right),\n                              onTap: () => navigator(context, AppWhitelist(proxyServer: proxyServer))),\n                          Divider(\n                              height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                          ListTile(\n                              title: Text(localizations.appBlacklist),\n                              trailing: const Icon(Icons.arrow_right),\n                              onTap: () => navigator(context, AppBlacklist(proxyServer: proxyServer)))\n                        ])\n                ]))));\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/menu/menu.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:io';\n\nimport 'package:date_format/date_format.dart';\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/ui/mobile/mobile.dart';\nimport 'package:proxypin/ui/mobile/setting/app_filter.dart';\nimport 'package:proxypin/ui/mobile/setting/report_servers.dart';\nimport 'package:proxypin/ui/mobile/setting/ssl.dart';\nimport 'package:proxypin/ui/mobile/widgets/highlight.dart';\nimport 'package:proxypin/ui/mobile/widgets/remote_device.dart';\n\n/// +号菜单\nclass MoreMenu extends StatelessWidget {\n  static bool sortDesc = true;\n\n  final ProxyServer proxyServer;\n  final ValueNotifier<RemoteModel> remoteDevice;\n\n  const MoreMenu({super.key, required this.proxyServer, required this.remoteDevice});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    return PopupMenuButton(\n      offset: const Offset(0, 30),\n      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),\n      elevation: 8,\n      color: Theme.of(context).colorScheme.surface,\n      child: const SizedBox(height: 38, width: 38, child: Icon(Icons.more_vert, size: 26)),\n      itemBuilder: (BuildContext context) {\n        return <PopupMenuEntry>[\n          PopupMenuItem(\n              height: 32,\n              child: ListTile(\n                  dense: true,\n                  title: Text(localizations.httpsProxy),\n                  leading:\n                      proxyServer.enableSsl ? Icon(Icons.lock_open) : Icon(Icons.https_outlined, color: Colors.red),\n                  onTap: () {\n                    navigator(context, MobileSslWidget(proxyServer: proxyServer));\n                  })),\n          if (Platform.isAndroid)\n            PopupMenuItem(\n                height: 32,\n                child: ListTile(\n                    dense: true,\n                    title: Text(localizations.appWhitelist),\n                    leading: const Icon(Icons.android_rounded),\n                    onTap: () {\n                      navigator(context, AppWhitelist(proxyServer: proxyServer));\n                    })),\n          PopupMenuItem(\n              height: 32,\n              child: ListTile(\n                dense: true,\n                leading: const Icon(Icons.devices),\n                title: Text(localizations.remoteDevice),\n                onTap: () {\n                  Navigator.maybePop(context);\n                  navigator(context, RemoteDevicePage(proxyServer: proxyServer, remoteDevice: remoteDevice));\n                },\n              )),\n          PopupMenuItem(\n              height: 32,\n              child: ListTile(\n                dense: true,\n                leading: const Icon(Icons.cloud_upload_outlined),\n                title: Text(localizations.reportServers),\n                onTap: () {\n                  Navigator.maybePop(context);\n                  navigator(context, const ReportServersPageMobile());\n                },\n              )),\n          const PopupMenuDivider(height: 0),\n          PopupMenuItem(\n              height: 32,\n              child: ListTile(\n                dense: true,\n                leading: const Icon(Icons.search),\n                title: Text(localizations.search),\n                onTap: () async {\n                  await Navigator.maybePop(context);\n\n                  MobileApp.searchStateKey.currentState?.showSearch();\n                },\n              )),\n          PopupMenuItem(\n              height: 32,\n              child: ListTile(\n                dense: true,\n                leading: const Icon(Icons.highlight_outlined),\n                title: Text('${localizations.keyword}${localizations.highlight}'),\n                onTap: () {\n                  navigator(context, const KeywordHighlight());\n                },\n              )),\n          PopupMenuItem(\n              height: 32,\n              child: ListTile(\n                dense: true,\n                leading: const Icon(Icons.share_outlined),\n                title: Text(localizations.viewExport),\n                onTap: () async {\n                  Navigator.maybePop(context);\n                  var name = formatDate(DateTime.now(), [m, '-', d, ' ', HH, ':', nn, ':', ss]);\n                  MobileApp.requestStateKey.currentState?.export(context, 'ProxyPin$name');\n                },\n              )),\n          PopupMenuItem(\n              height: 32,\n              child: ListTile(\n                dense: true,\n                leading: const Icon(Icons.sort, size: 16),\n                title: Text(sortDesc ? localizations.timeAsc : localizations.timeDesc),\n                onTap: () async {\n                  await Navigator.maybePop(context);\n\n                  sortDesc = !sortDesc;\n                  MobileApp.requestStateKey.currentState?.sort(sortDesc);\n                },\n              )),\n        ];\n      },\n    );\n  }\n\n  void navigator(BuildContext context, Widget widget) async {\n    await Navigator.maybePop(context);\n    if (context.mounted) {\n      Navigator.of(context).push(\n        MaterialPageRoute(builder: (BuildContext context) => widget),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/mobile.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/native/app_lifecycle.dart';\nimport 'package:proxypin/native/pip.dart';\nimport 'package:proxypin/native/vpn.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/bin/listener.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/websocket.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/ui/component/memory_cleanup.dart';\nimport 'package:proxypin/ui/toolbox/toolbox.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/content/panel.dart';\nimport 'package:proxypin/ui/launch/launch.dart';\nimport 'package:proxypin/ui/mobile/menu/drawer.dart';\nimport 'package:proxypin/ui/mobile/menu/bottom_navigation.dart';\nimport 'package:proxypin/ui/mobile/menu/menu.dart';\nimport 'package:proxypin/ui/mobile/request/list.dart';\nimport 'package:proxypin/ui/mobile/request/search.dart';\nimport 'package:proxypin/ui/mobile/widgets/pip.dart';\nimport 'package:proxypin/ui/mobile/widgets/remote_device.dart';\nimport 'package:proxypin/utils/ip.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\nimport 'package:proxypin/utils/navigator.dart';\n\nimport '../app_update/app_update_repository.dart';\nimport 'package:proxypin/ui/component/multi_window.dart';\nimport 'package:proxypin/ui/mobile/debug/breakpoint_executor.dart';\n\n///移动端首页\n///@author wanghongen\nclass MobileHomePage extends StatefulWidget {\n  final Configuration configuration;\n  final AppConfiguration appConfiguration;\n\n  const MobileHomePage(this.configuration, this.appConfiguration, {super.key});\n\n  @override\n  State<StatefulWidget> createState() {\n    return MobileHomeState();\n  }\n}\n\nclass MobileApp {\n  ///请求列表key\n  static final GlobalKey<RequestListState> requestStateKey = GlobalKey<RequestListState>();\n\n  ///搜索key\n  static final GlobalKey<MobileSearchState> searchStateKey = GlobalKey<MobileSearchState>();\n\n  ///请求列表容器\n  static final container = ListenableList<HttpRequest>();\n}\n\nclass MobileHomeState extends State<MobileHomePage> implements EventListener, LifecycleListener {\n  /// 选择索引\n  final ValueNotifier<int> _selectIndex = ValueNotifier(0);\n\n  late ProxyServer proxyServer;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void onRequest(Channel channel, HttpRequest request) {\n    MobileApp.requestStateKey.currentState!.add(channel, request);\n    PictureInPicture.addData(request.requestUrl);\n\n    //监控内存 到达阈值清理\n    MemoryCleanupMonitor.onMonitor(onCleanup: () {\n      MobileApp.requestStateKey.currentState?.cleanupEarlyData(32);\n    });\n  }\n\n  @override\n  void onResponse(ChannelContext channelContext, HttpResponse response) {\n    MobileApp.requestStateKey.currentState!.addResponse(channelContext, response);\n  }\n\n  @override\n  void onMessage(Channel channel, HttpMessage message, WebSocketFrame frame) {\n    var panel = NetworkTabController.current;\n    if (panel?.request.get() == message || panel?.response.get() == message) {\n      panel?.changeState();\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    AppLifecycleBinding.instance.addListener(this);\n    proxyServer = ProxyServer(widget.configuration);\n    proxyServer.addListener(this);\n    proxyServer.start();\n\n    if (widget.appConfiguration.upgradeNoticeV26) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        showUpgradeNotice();\n      });\n    } else if (Platform.isAndroid) {\n      AppUpdateRepository.checkUpdate(context);\n    }\n\n    // Handle breakpoint window on mobile\n    MultiWindow.onOpenWindow = (widgetName, args) async {\n      if (widgetName == 'BreakpointExecutor' && args != null) {\n        if (!mounted) return;\n        Navigator.push(\n          context,\n          MaterialPageRoute(\n            builder: (context) => BreakpointExecutor(\n              requestId: args['requestId'],\n              request: HttpRequest.fromJson(jsonDecode(jsonEncode(args['request']))),\n              response:\n                  args['response'] == null ? null : HttpResponse.fromJson(jsonDecode(jsonEncode(args['response']))),\n              isResponse: args['type'] == 'response',\n            ),\n          ),\n        );\n      }\n    };\n  }\n\n  @override\n  void dispose() {\n    AppLifecycleBinding.instance.removeListener(this);\n    super.dispose();\n  }\n\n  int exitTime = 0;\n\n  var requestPageNavigatorKey = GlobalKey<NavigatorState>();\n  var toolboxNavigatorKey = GlobalKey<NavigatorState>();\n  var configNavigatorKey = GlobalKey<NavigatorState>();\n  var settingNavigatorKey = GlobalKey<NavigatorState>();\n\n  @override\n  Widget build(BuildContext context) {\n    var navigationView = [\n      NavigatorPage(\n          navigatorKey: requestPageNavigatorKey,\n          child: RequestPage(proxyServer: proxyServer, appConfiguration: widget.appConfiguration)),\n      NavigatorPage(\n          navigatorKey: toolboxNavigatorKey,\n          child: Scaffold(\n              appBar: PreferredSize(\n                  preferredSize: const Size.fromHeight(42),\n                  child: AppBar(\n                      title: Text(localizations.toolbox,\n                          style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w400)),\n                      centerTitle: true)),\n              body: Toolbox(proxyServer: proxyServer))),\n      NavigatorPage(navigatorKey: configNavigatorKey, child: ConfigPage(proxyServer: proxyServer)),\n      NavigatorPage(\n          navigatorKey: settingNavigatorKey,\n          child: SettingPage(proxyServer: proxyServer, appConfiguration: widget.appConfiguration)),\n    ];\n\n    if (!widget.appConfiguration.bottomNavigation) _selectIndex.value = 0;\n\n    return PopScope(\n        canPop: false,\n        onPopInvokedWithResult: (didPop, result) async {\n          if (didPop) {\n            return;\n          }\n\n          if (navigationView[_selectIndex.value].onPopInvoked()) {\n            return;\n          }\n\n          if (await enterPictureInPicture()) {\n            return;\n          }\n\n          if (DateTime.now().millisecondsSinceEpoch - exitTime > 1500) {\n            exitTime = DateTime.now().millisecondsSinceEpoch;\n            if (mounted) {\n              FlutterToastr.show(localizations.appExitTips, this.context,\n                  rootNavigator: true, duration: FlutterToastr.lengthLong);\n            }\n            return;\n          }\n          //退出程序\n          SystemNavigator.pop();\n        },\n        child: ValueListenableBuilder<int>(\n            valueListenable: _selectIndex,\n            builder: (context, index, child) => Scaffold(\n                body: IndexedStack(index: index, children: navigationView),\n                bottomNavigationBar: widget.appConfiguration.bottomNavigation\n                    ? Container(\n                        constraints: const BoxConstraints(maxHeight: 85),\n                        // padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),\n                        child: ClipRRect(\n                            // borderRadius: BorderRadius.circular(16),\n                            child: Theme(\n                          data: Theme.of(context).copyWith(splashColor: Colors.transparent),\n                          child: BottomNavigationBar(\n                            type: BottomNavigationBarType.fixed,\n                            iconSize: 23,\n                            showSelectedLabels: false,\n                            showUnselectedLabels: false,\n                            selectedFontSize: 2,\n                            unselectedFontSize: 2,\n                            elevation: 0,\n                            items: [\n                              BottomNavigationBarItem(\n                                  tooltip: localizations.requests,\n                                  icon: const Icon(Icons.workspaces_outlined),\n                                  label: localizations.requests),\n                              BottomNavigationBarItem(\n                                  tooltip: localizations.toolbox,\n                                  icon: const Icon(Icons.hardware_outlined),\n                                  label: localizations.toolbox),\n                              BottomNavigationBarItem(\n                                  tooltip: localizations.config,\n                                  icon: const Icon(Icons.description_outlined),\n                                  label: localizations.config),\n                              BottomNavigationBarItem(\n                                  tooltip: localizations.setting,\n                                  icon: const Icon(Icons.settings_outlined),\n                                  label: localizations.setting),\n                            ],\n                            currentIndex: _selectIndex.value,\n                            onTap: (index) => _selectIndex.value = index,\n                          ),\n                        )))\n                    : null)));\n  }\n\n  @override\n  void onUserLeaveHint() {\n    enterPictureInPicture();\n  }\n\n  Future<bool> enterPictureInPicture() async {\n    if (Vpn.isVpnStarted) {\n      if (!Platform.isAndroid || !(await (AppConfiguration.instance)).pipEnabled.value) {\n        return false;\n      }\n\n      List<String>? appList =\n          proxyServer.configuration.appWhitelistEnabled ? proxyServer.configuration.appWhitelist : [];\n      List<String>? disallowApps;\n      if (appList.isEmpty) {\n        disallowApps = proxyServer.configuration.appBlacklist ?? [];\n      }\n\n      return PictureInPicture.enterPictureInPictureMode(\n          Platform.isAndroid ? await localIp() : \"127.0.0.1\", proxyServer.port,\n          appList: appList, disallowApps: disallowApps);\n    }\n    return false;\n  }\n\n  @override\n  onPictureInPictureModeChanged(bool isInPictureInPictureMode) async {\n    if (isInPictureInPictureMode) {\n      Navigator.push(\n          context,\n          PageRouteBuilder(\n              transitionDuration: Duration.zero,\n              reverseTransitionDuration: Duration.zero,\n              pageBuilder: (context, animation, secondaryAnimation) {\n                return PictureInPictureWindow(MobileApp.container);\n              }));\n      return;\n    }\n\n    if (!isInPictureInPictureMode) {\n      Navigator.maybePop(context);\n      Vpn.isRunning().then((value) {\n        Vpn.isVpnStarted = value;\n        SocketLaunch.startStatus.value = ValueWrap.of(value);\n      });\n    }\n  }\n\n  void showUpgradeNotice() {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    String content = isCN\n        ? '提示：默认不会开启HTTPS抓包，请安装证书后再开启HTTPS抓包。\\n\\n'\n            '1. 新增请求断点，可修改请求、响应后发送；\\n'\n            '2. 在请求编辑器中为Header添加自动补全建议；\\n'\n            '3. Android、iOS新增系统代理IP忽略设置；\\n'\n            '4. Android新增系统代理是否启用设置；\\n'\n            '5. Socks5代理新增 IPV6 支持；\\n'\n            '6. 修复 MacOS 网线代理设置失败问题；\\n'\n        : 'Note: HTTPS capture is disabled by default — please install the certificate before enabling HTTPS capture.\\n\\n'\n            '1. Added request breakpoint feature, allowing modification of requests and responses before sending;\\n'\n            '2. Added autocomplete suggestions for HTTP headers in request editor;\\n'\n            '3. Added system proxy IP ignore settings for Android and iOS;\\n'\n            '4. Added system proxy enable/disable settings for Android;\\n'\n            '5. Added IPv6 support for Socks5 proxy;\\n'\n            '6. Fixed an issue where proxy settings failed on macOS; \\n';\n    showAlertDialog(isCN ? '更新内容V${AppConfiguration.version}' : \"What's new in V${AppConfiguration.version}\", content,\n        () {\n      widget.appConfiguration.upgradeNoticeV26 = false;\n      widget.appConfiguration.flushConfig();\n    });\n  }\n\n  void showAlertDialog(String title, String content, Function onClose) {\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (context) {\n          return AlertDialog(\n              scrollable: true,\n              actions: [\n                TextButton(\n                    onPressed: () {\n                      onClose.call();\n                      Navigator.pop(context);\n                    },\n                    child: Text(localizations.close))\n              ],\n              title: Text(title, style: const TextStyle(fontSize: 18)),\n              content: SelectableText(content));\n        });\n  }\n}\n\nclass RequestPage extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final AppConfiguration appConfiguration;\n\n  const RequestPage({super.key, required this.proxyServer, required this.appConfiguration});\n\n  @override\n  State<RequestPage> createState() => RequestPageState();\n}\n\nclass RequestPageState extends State<RequestPage> {\n  /// 远程连接\n  final ValueNotifier<RemoteModel> remoteDevice = ValueNotifier(RemoteModel(connect: false));\n\n  late ProxyServer proxyServer;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    proxyServer = widget.proxyServer;\n\n    //远程连接\n    remoteDevice.addListener(() {\n      if (remoteDevice.value.connect) {\n        proxyServer.configuration.remoteHost = \"http://${remoteDevice.value.host}:${remoteDevice.value.port}\";\n        checkConnectTask(context);\n      } else {\n        proxyServer.configuration.remoteHost = null;\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    remoteDevice.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(children: [\n      Scaffold(\n        appBar: _MobileAppBar(widget.appConfiguration, proxyServer, remoteDevice: remoteDevice),\n        drawer: widget.appConfiguration.bottomNavigation\n            ? null\n            : DrawerWidget(proxyServer: proxyServer, container: MobileApp.container),\n        floatingActionButton: _launchActionButton(),\n        body: ValueListenableBuilder(\n            valueListenable: remoteDevice,\n            builder: (context, value, _) {\n              return Column(children: [\n                value.connect ? remoteConnect(value) : const SizedBox(),\n                Expanded(\n                    child: RequestListWidget(\n                        key: MobileApp.requestStateKey, proxyServer: proxyServer, list: MobileApp.container))\n              ]);\n            }),\n      ),\n      PictureInPictureIcon(proxyServer),\n    ]);\n  }\n\n  Widget _launchActionButton() {\n    var theme = Theme.of(context);\n    return Theme(\n        data: ThemeData.from(colorScheme: theme.colorScheme, textTheme: theme.textTheme, useMaterial3: true),\n        child: FloatingActionButton(\n          onPressed: null,\n          backgroundColor: theme.colorScheme.primaryContainer,\n          child: SocketLaunch(\n              proxyServer: proxyServer,\n              size: 36,\n              startup: proxyServer.configuration.startup,\n              serverLaunch: false,\n              onStart: () async {\n                String host = Platform.isAndroid ? await localIp(readCache: false) : \"127.0.0.1\";\n                int port = proxyServer.port;\n                if (Platform.isIOS) {\n                  await proxyServer.retryBind();\n                }\n\n                if (remoteDevice.value.ipProxy == true) {\n                  host = remoteDevice.value.host!;\n                  port = remoteDevice.value.port!;\n                }\n\n                Vpn.startVpn(host, port, proxyServer.configuration, ipProxy: remoteDevice.value.ipProxy);\n              },\n              onStop: () => Vpn.stopVpn()),\n        ));\n  }\n\n  /// 远程连接\n  Widget remoteConnect(RemoteModel value) {\n    return Container(\n        margin: const EdgeInsets.only(top: 5, bottom: 5),\n        height: 56,\n        width: double.infinity,\n        child: ElevatedButton(\n          style: ButtonStyle(\n              shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                  RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),\n          onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {\n            return RemoteDevicePage(remoteDevice: remoteDevice, proxyServer: proxyServer);\n          })),\n          child: Text(localizations.remoteConnected(remoteDevice.value.os ?? ', ${remoteDevice.value.hostname}'),\n              style: Theme.of(context).textTheme.titleMedium),\n        ));\n  }\n\n  /// 检查远程连接\n  Future<void> checkConnectTask(BuildContext context) async {\n    int retry = 0;\n    Timer.periodic(const Duration(milliseconds: 15000), (timer) async {\n      if (remoteDevice.value.connect == false) {\n        timer.cancel();\n        return;\n      }\n\n      try {\n        var response = await HttpClients.get(\"http://${remoteDevice.value.host}:${remoteDevice.value.port}/ping\")\n            .timeout(const Duration(seconds: 3));\n        if (response.bodyAsString == \"pong\") {\n          retry = 0;\n          return;\n        }\n      } catch (e) {\n        retry++;\n      }\n\n      if (retry > 3) {\n        if (context.mounted) {\n          ScaffoldMessenger.of(context).removeCurrentSnackBar();\n\n          ScaffoldMessenger.of(context).showSnackBar(SnackBar(\n              content: Text(localizations.remoteConnectDisconnect),\n              action: SnackBarAction(\n                  label: localizations.disconnect,\n                  onPressed: () {\n                    timer.cancel();\n                    remoteDevice.value = RemoteModel(connect: false);\n                  })));\n        }\n      }\n    });\n  }\n}\n\n/// 移动端AppBar\nclass _MobileAppBar extends StatelessWidget implements PreferredSizeWidget {\n  final AppConfiguration appConfiguration;\n  final ProxyServer proxyServer;\n  final ValueNotifier<RemoteModel> remoteDevice;\n\n  const _MobileAppBar(this.appConfiguration, this.proxyServer, {required this.remoteDevice});\n\n  @override\n  Size get preferredSize => const Size.fromHeight(42);\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    var bottomNavigation = appConfiguration.bottomNavigation;\n\n    return AppBar(\n        leading: bottomNavigation ? const SizedBox() : null,\n        systemOverlayStyle:\n            Platform.isAndroid ? SystemUiOverlayStyle(systemNavigationBarColor: ColorScheme.of(context).surface) : null,\n        title: MobileSearch(\n            key: MobileApp.searchStateKey, onSearch: (val) => MobileApp.requestStateKey.currentState?.search(val)),\n        actions: [\n          IconButton(\n              tooltip: localizations.clear,\n              icon: const Icon(Icons.delete_outline),\n              onPressed: () => MobileApp.requestStateKey.currentState?.clean()),\n          const SizedBox(width: 2),\n          MoreMenu(proxyServer: proxyServer, remoteDevice: remoteDevice),\n          const SizedBox(width: 10),\n        ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/request/domians.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:collection';\n\nimport 'package:date_format/date_format.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/components/host_filter.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/mobile/request/request_sequence.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\n\n///域名列表\n///@author wanghongen\nclass DomainList extends StatefulWidget {\n  final ListenableList<HttpRequest> list;\n  final ProxyServer proxyServer;\n  final Function(List<HttpRequest>)? onRemove;\n\n  const DomainList({super.key, required this.list, required this.proxyServer, this.onRemove});\n\n  @override\n  State<StatefulWidget> createState() {\n    return DomainListState();\n  }\n}\n\nclass DomainListState extends State<DomainList> with AutomaticKeepAliveClientMixin {\n  final ScrollController _scrollController = ScrollController();\n\n  GlobalKey<RequestSequenceState> requestSequenceKey = GlobalKey<RequestSequenceState>();\n  late Configuration configuration;\n\n  //域名和对应请求列表的映射\n  Map<HostAndPort, List<HttpRequest>> containerMap = {};\n\n  //域名列表 为了维护插入顺序\n  LinkedHashSet<HostAndPort> domainList = LinkedHashSet<HostAndPort>();\n\n  //显示的域名 最新的在顶部\n  List<HostAndPort> view = [];\n\n  HostAndPort? showHostAndPort;\n\n  //搜索关键字\n  String? searchText;\n\n  bool changing = false;\n\n  bool sortDesc = true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  initState() {\n    super.initState();\n    configuration = widget.proxyServer.configuration;\n    initFromContainer();\n  }\n\n  initFromContainer() {\n    for (var request in widget.list) {\n      var hostAndPort = request.hostAndPort!;\n      domainList.add(hostAndPort);\n      var list = containerMap[hostAndPort] ??= [];\n      list.add(request);\n    }\n\n    view = domainList.toList();\n  }\n\n  add(HttpRequest request) {\n    var hostAndPort = request.hostAndPort!;\n    domainList.remove(hostAndPort);\n    domainList.add(hostAndPort);\n\n    var list = containerMap[hostAndPort] ??= [];\n    list.add(request);\n    if (showHostAndPort == request.hostAndPort) {\n      requestSequenceKey.currentState?.add(request);\n    }\n\n    if (!filter(request.hostAndPort!)) {\n      return;\n    }\n\n    view = [...domainList.where(filter)].reversed.toList();\n    changeState();\n  }\n\n  addResponse(HttpResponse response) {\n    HostAndPort? hostAndPort = response.request!.hostAndPort;\n    if (response.isWebSocket) {\n      add(response.request!);\n    }\n\n    if (showHostAndPort == hostAndPort) {\n      requestSequenceKey.currentState?.addResponse(response);\n    }\n  }\n\n  clean() {\n    setState(() {\n      view.clear();\n      domainList.clear();\n      containerMap.clear();\n\n      initFromContainer();\n    });\n  }\n\n  remove(List<HttpRequest> list) {\n    for (var request in list) {\n      containerMap[request.hostAndPort]?.remove(request);\n      if (containerMap[request.hostAndPort]!.isEmpty) {\n        domainList.remove(request.hostAndPort);\n        view.remove(request.hostAndPort);\n      }\n    }\n\n    setState(() {});\n  }\n\n  ///搜索域名\n  void search(String? text) {\n    if (text == null) {\n      setState(() {\n        view = List.of(domainList.toList().reversed);\n        searchText = null;\n      });\n      return;\n    }\n\n    text = text.toLowerCase();\n\n    var contains = text.contains(searchText ?? \"\");\n    searchText = text.toLowerCase();\n    if (contains) {\n      //包含从上次结果过滤\n      view.retainWhere(filter);\n    } else {\n      view = List.of(domainList.where(filter).toList().reversed);\n    }\n    changeState();\n  }\n\n  ///排序\n  sort(bool desc) {\n    sortDesc = desc;\n  }\n\n  bool filter(HostAndPort hostAndPort) {\n    if (searchText?.isNotEmpty == true) {\n      return hostAndPort.domain.toLowerCase().contains(searchText!);\n    }\n    return true;\n  }\n\n  changeState() {\n    //防止频繁刷新\n    if (!changing) {\n      changing = true;\n      Future.delayed(const Duration(milliseconds: 350), () {\n        setState(() {\n          changing = false;\n        });\n      });\n    }\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    return Scrollbar(\n        controller: _scrollController,\n        child: ListView.separated(\n            controller: _scrollController,\n            padding: EdgeInsets.zero,\n            separatorBuilder: (context, index) =>\n                Divider(thickness: 0.2, height: 0.5, color: Theme.of(context).dividerColor),\n            itemCount: view.length,\n            itemBuilder: (ctx, index) => title(index)));\n  }\n\n  Widget title(int index) {\n    var value = containerMap[view.elementAt(index)];\n    var time = value == null ? '' : formatDate(value.last.requestTime, [m, '/', d, ' ', HH, ':', nn, ':', ss]);\n\n    return ListTile(\n        visualDensity: const VisualDensity(vertical: -4),\n        title: Text(view.elementAt(index).domain, maxLines: 1, overflow: TextOverflow.ellipsis),\n        trailing: const Icon(Icons.chevron_right),\n        subtitle: Text(localizations.domainListSubtitle(value?.length ?? '', time),\n            maxLines: 1, overflow: TextOverflow.ellipsis),\n        onLongPress: () => menu(index),\n        // show menus\n        contentPadding: const EdgeInsets.only(left: 10),\n        onTap: () {\n          Navigator.push(context, MaterialPageRoute(builder: (context) {\n            showHostAndPort = view.elementAt(index);\n            var list = containerMap[view.elementAt(index)];\n\n            return Scaffold(\n                appBar: AppBar(title: Text(view.elementAt(index).domain, style: const TextStyle(fontSize: 16))),\n                body: RequestSequence(\n                    key: requestSequenceKey,\n                    displayDomain: false,\n                    container: ListenableList(sortDesc ? list : list?.reversed.toList()),\n                    sortDesc: sortDesc,\n                    onRemove: widget.onRemove,\n                    proxyServer: widget.proxyServer));\n          }));\n        });\n  }\n\n  scrollToTop() {\n    _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);\n  }\n\n  ///菜单\n  menu(int index) {\n    var hostAndPort = view.elementAt(index);\n\n    showModalBottomSheet(\n        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),\n        context: context,\n        enableDrag: true,\n        builder: (ctx) {\n          return Wrap(\n            alignment: WrapAlignment.center,\n            children: [\n              BottomSheetItem(\n                  text: localizations.copyHost,\n                  onPressed: () {\n                    Clipboard.setData(ClipboardData(text: hostAndPort.host));\n                    FlutterToastr.show(localizations.copied, context);\n                  }),\n              const Divider(thickness: 0.5, height: 5),\n              BottomSheetItem(\n                  text: localizations.addBlacklist,\n                  onPressed: () {\n                    HostFilter.blacklist.add(hostAndPort.host);\n                    configuration.flushConfig();\n                    FlutterToastr.show(localizations.addSuccess, context);\n                  }),\n              const Divider(thickness: 0.5, height: 5),\n              BottomSheetItem(\n                  text: localizations.addWhitelist,\n                  onPressed: () {\n                    HostFilter.whitelist.add(hostAndPort.host);\n                    configuration.flushConfig();\n                    FlutterToastr.show(localizations.addSuccess, context);\n                  }),\n              const Divider(thickness: 0.5, height: 5),\n              BottomSheetItem(\n                  text: localizations.deleteWhitelist,\n                  onPressed: () {\n                    HostFilter.whitelist.remove(hostAndPort.host);\n                    configuration.flushConfig();\n                    FlutterToastr.show(localizations.deleteSuccess, context);\n                  }),\n              const Divider(thickness: 0.5, height: 5),\n              BottomSheetItem(\n                  text: localizations.repeatDomainRequests,\n                  onPressed: () {\n                    repeatDomainRequests(hostAndPort);\n                  }),\n              const Divider(thickness: 0.5, height: 5),\n              BottomSheetItem(\n                  text: localizations.delete,\n                  onPressed: () {\n                    setState(() {\n                      var requests = containerMap.remove(hostAndPort);\n                      domainList.remove(hostAndPort);\n                      view.removeAt(index);\n                      if (requests != null) {\n                        widget.onRemove?.call(requests);\n                      }\n                      FlutterToastr.show(localizations.deleteSuccess, context);\n                    });\n                  }),\n              Container(\n                color: Theme.of(context).hoverColor,\n                height: 8,\n              ),\n              TextButton(\n                child: Container(\n                    height: 45,\n                    width: double.infinity,\n                    padding: const EdgeInsets.only(top: 10),\n                    child: Text(localizations.cancel, textAlign: TextAlign.center)),\n                onPressed: () {\n                  Navigator.of(ctx).pop();\n                },\n              ),\n            ],\n          );\n        });\n  }\n\n  //重复域名下请求\n  void repeatDomainRequests(HostAndPort hostAndPort) async {\n    var requests = containerMap[hostAndPort];\n    if (requests == null) return;\n\n    for (var httpRequest in requests.toList()) {\n      var request = httpRequest.copy(uri: httpRequest.requestUrl);\n      var proxyInfo = widget.proxyServer.isRunning ? ProxyInfo.of(\"127.0.0.1\", widget.proxyServer.port) : null;\n      try {\n        await HttpClients.proxyRequest(request, proxyInfo: proxyInfo);\n        if (mounted) FlutterToastr.show(localizations.reSendRequest, rootNavigator: true, context);\n      } catch (e) {\n        if (mounted) FlutterToastr.show('${localizations.fail}$e', rootNavigator: true, context);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/request/favorite.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:collection';\nimport 'dart:io';\nimport 'dart:convert';\n\nimport 'package:date_format/date_format.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/network/components/manager/script_manager.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/storage/favorites.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/content/panel.dart';\nimport 'package:proxypin/ui/mobile/request/repeat.dart';\nimport 'package:proxypin/ui/mobile/request/request_editor.dart';\nimport 'package:proxypin/ui/mobile/setting/request_rewrite.dart';\nimport 'package:proxypin/ui/mobile/setting/script.dart';\nimport 'package:proxypin/utils/curl.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\nimport 'package:file_picker/file_picker.dart';\n\n/// 收藏列表页面\n/// @author WangHongEn\nclass MobileFavorites extends StatefulWidget {\n  final ProxyServer proxyServer;\n\n  const MobileFavorites({super.key, required this.proxyServer});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _FavoritesState();\n  }\n}\n\nclass _FavoritesState extends State<MobileFavorites> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  Future<void> _exportJson() async {\n    final favorites = await FavoriteStorage.favorites;\n    final json = FavoriteStorage.toJson(favorites);\n    final bytes = utf8.encode(json);\n    final path = await FilePicker.platform.saveFile(fileName: 'favorites.json', bytes: bytes);\n    if (path == null) return;\n    if (mounted) FlutterToastr.show(localizations.exportSuccess, context);\n  }\n\n  Future<String?> _materializePickedFile(PlatformFile file) async {\n    if (file.path != null) return file.path!;\n    if (file.bytes == null) return null;\n    final tmp = await File('${Directory.systemTemp.path}/${file.name}').create();\n    await tmp.writeAsBytes(file.bytes!, flush: true);\n    return tmp.path;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n            title: Text(localizations.favorites, style: const TextStyle(fontSize: 16)),\n            centerTitle: true,\n            actions: [\n              IconButton(\n                  tooltip: localizations.export,\n                  icon: const Icon(Icons.upload_file, size: 20),\n                  onPressed: () async {\n                    try {\n                      await _exportJson();\n                    } catch (e) {\n                      if (context.mounted) FlutterToastr.show('${localizations.importFailed}: $e', context);\n                    }\n                  }),\n              IconButton(\n                  tooltip: localizations.import,\n                  icon: const Icon(Icons.download_for_offline_outlined, size: 20),\n                  onPressed: () async {\n                    final result = await FilePicker.platform\n                        .pickFiles(type: FileType.custom, allowedExtensions: ['json', 'har'], withData: true);\n                    final file = result?.files.isNotEmpty == true ? result!.files.first : null;\n                    if (file == null) return;\n                    final path = await _materializePickedFile(file);\n                    if (path == null) return;\n                    try {\n                      await FavoriteStorage.importFromFile(path);\n                      if (context.mounted) FlutterToastr.show(localizations.importSuccess, context);\n                      setState(() {});\n                    } catch (e) {\n                      if (context.mounted) FlutterToastr.show('${localizations.importFailed}: $e', context);\n                    }\n                  }),\n            ]),\n        body: FutureBuilder(\n            future: FavoriteStorage.favorites,\n            builder: (BuildContext context, AsyncSnapshot<Queue<Favorite>> snapshot) {\n              if (snapshot.hasData) {\n                var favorites = snapshot.data ?? Queue();\n                if (favorites.isEmpty) {\n                  return Center(child: Text(localizations.emptyFavorite));\n                }\n\n                return ListView.separated(\n                  itemCount: favorites.length,\n                  itemBuilder: (_, index) {\n                    var favorite = favorites.elementAt(index);\n                    return _FavoriteItem(\n                      favorite,\n                      index: index,\n                      onRemove: (Favorite favorite) async {\n                        await FavoriteStorage.removeFavorite(favorite);\n                        setState(() {});\n                      },\n                      proxyServer: widget.proxyServer,\n                    );\n                  },\n                  separatorBuilder: (_, __) => const Divider(height: 1, thickness: 0.3),\n                );\n              } else {\n                return const SizedBox();\n              }\n            }));\n  }\n}\n\nclass _FavoriteItem extends StatefulWidget {\n  final int index;\n  final Favorite favorite;\n  final ProxyServer proxyServer;\n  final Function(Favorite favorite)? onRemove;\n\n  const _FavoriteItem(this.favorite, {required this.onRemove, required this.proxyServer, required this.index});\n\n  @override\n  State<_FavoriteItem> createState() => _FavoriteItemState();\n}\n\nclass _FavoriteItemState extends State<_FavoriteItem> {\n  late HttpRequest request;\n  bool selected = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    request = widget.favorite.request;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    request = widget.favorite.request;\n\n    var response = request.response;\n    Widget? title = widget.favorite.name?.isNotEmpty == true\n        ? Text(widget.favorite.name!,\n            overflow: TextOverflow.ellipsis,\n            maxLines: 1,\n            style: TextStyle(fontSize: 14, color: Colors.blueAccent.shade200))\n        : Text.rich(\n            overflow: TextOverflow.ellipsis,\n            maxLines: 2,\n            TextSpan(children: [\n              TextSpan(text: '${request.method.name} ', style: const TextStyle(fontSize: 14, color: Colors.teal)),\n              TextSpan(\n                text: request.remoteDomain(),\n                style: TextStyle(fontSize: 14, color: Colors.blue),\n              ),\n              TextSpan(\n                text: request.path,\n                style: TextStyle(fontSize: 14, color: Colors.green),\n              ),\n              if (request.requestUri?.query.isNotEmpty == true)\n                TextSpan(\n                    text: '?${request.requestUri?.query}',\n                    style: TextStyle(fontSize: 14, color: Colors.pinkAccent.shade200))\n            ]));\n\n    var time = formatDate(request.requestTime, [mm, '-', d, ' ', HH, ':', nn, ':', ss]);\n    String subtitle =\n        '$time - [${response?.status.code ?? ''}]  ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''} ';\n\n    return GestureDetector(\n        onLongPressStart: menu,\n        child: ListTile(\n            selected: selected,\n            minLeadingWidth: 25,\n            leading: getIcon(response),\n            title: title,\n            trailing: request.isWebSocket\n                ? Text(\n                    'WS',\n                    style: TextStyle(\n                      fontSize: 10,\n                      fontWeight: FontWeight.w600,\n                      color: Theme.of(context).colorScheme.primary,\n                    ),\n                  )\n                : null,\n            subtitle: Text.rich(\n                maxLines: 1,\n                TextSpan(children: [\n                  TextSpan(text: '#${widget.index} ', style: const TextStyle(fontSize: 12, color: Colors.teal)),\n                  TextSpan(text: subtitle, style: const TextStyle(fontSize: 12)),\n                ])),\n            dense: true,\n            onTap: onClick));\n  }\n\n  ///右键菜单\n  void menu(details) {\n    // setState(() {\n    //   selected = true;\n    // });\n\n    var globalPosition = details.globalPosition;\n    MediaQueryData mediaQuery = MediaQuery.of(context);\n    var position = RelativeRect.fromLTRB(globalPosition.dx, globalPosition.dy, globalPosition.dx, globalPosition.dy);\n    // Trigger haptic feedback\n    if (Platform.isAndroid) HapticFeedback.mediumImpact();\n\n    showMenu(\n        context: context,\n        constraints: BoxConstraints(maxWidth: mediaQuery.size.width * 0.88),\n        position: position,\n        items: [\n          //复制url\n          PopupMenuContainer(\n              child: Column(\n            children: [\n              Align(\n                alignment: Alignment.centerLeft,\n                child: Padding(\n                    padding: EdgeInsets.only(left: 20, top: 5),\n                    child: Text(localizations.selectAction, style: Theme.of(context).textTheme.bodyLarge)),\n              ),\n              //copy\n              menuItem(\n                left: itemButton(\n                    onPressed: () {\n                      Clipboard.setData(ClipboardData(text: request.requestUrl)).then((value) {\n                        if (mounted) {\n                          FlutterToastr.show(localizations.copied, context);\n                          Navigator.maybePop(context);\n                        }\n                      });\n                    },\n                    label: localizations.copyUrl,\n                    icon: Icons.link,\n                    iconSize: 22),\n                right: itemButton(\n                    onPressed: () {\n                      Clipboard.setData(ClipboardData(text: curlRequest(request))).then((value) {\n                        if (mounted) {\n                          FlutterToastr.show(localizations.copied, context);\n                          Navigator.maybePop(context);\n                        }\n                      });\n                    },\n                    label: localizations.copyCurl,\n                    icon: Icons.code),\n              ),\n              //repeat\n              menuItem(\n                left: itemButton(\n                    onPressed: () {\n                      onRepeat(request);\n                      Navigator.maybePop(context);\n                    },\n                    label: localizations.repeat,\n                    icon: Icons.repeat_one),\n                right: itemButton(\n                    onPressed: () => showCustomRepeat(request), label: localizations.customRepeat, icon: Icons.repeat),\n              ),\n              //favorite and edit\n              menuItem(\n                left: itemButton(\n                    onPressed: () {\n                      Navigator.of(context).pop();\n                      rename(widget.favorite);\n                    },\n                    label: localizations.rename,\n                    icon: Icons.drive_file_rename_outline),\n                right: itemButton(\n                    onPressed: () async {\n                      Navigator.pop(context);\n\n                      var pageRoute = MaterialPageRoute(\n                          builder: (context) => MobileRequestEditor(request: request, proxyServer: widget.proxyServer));\n                      Navigator.push(context, pageRoute);\n                    },\n                    label: localizations.editRequest,\n                    icon: Icons.replay_outlined),\n              ),\n\n              //script and rewrite\n              menuItem(\n                left: itemButton(\n                    onPressed: () async {\n                      Navigator.maybePop(context);\n                      var scriptManager = await ScriptManager.instance;\n                      var url = request.domainPath;\n                      var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.urls.contains(url));\n                      String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem);\n\n                      var pageRoute = MaterialPageRoute(\n                          builder: (context) =>\n                              ScriptEdit(scriptItem: scriptItem, script: script, urls: scriptItem?.urls ?? [url]));\n                      if (mounted) Navigator.push(context, pageRoute);\n                    },\n                    label: localizations.script,\n                    icon: Icons.javascript_outlined),\n                right: itemButton(\n                    onPressed: () async {\n                      Navigator.maybePop(context);\n                      bool isRequest = request.response == null;\n                      var requestRewrites = await RequestRewriteManager.instance;\n\n                      var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace;\n                      var rule = requestRewrites.getRequestRewriteRule(request, ruleType);\n\n                      var rewriteItems = await requestRewrites.getRewriteItems(rule);\n\n                      var pageRoute = MaterialPageRoute(\n                          builder: (_) => RewriteRule(rule: rule, items: rewriteItems, request: request));\n                      if (mounted) Navigator.push(context, pageRoute);\n                    },\n                    label: localizations.requestRewrite,\n                    icon: Icons.edit_outlined),\n              ),\n              SizedBox(height: 2),\n              Row(mainAxisAlignment: MainAxisAlignment.center, children: [\n                itemButton(\n                    onPressed: () {\n                      widget.onRemove?.call(widget.favorite);\n                      FlutterToastr.show(localizations.deleteSuccess, context);\n                      Navigator.maybePop(context);\n                    },\n                    label: localizations.deleteFavorite,\n                    icon: Icons.delete_outline),\n                SizedBox(width: 10),\n              ]),\n            ],\n          )),\n        ]).then((value) {\n      selected = false;\n      // if (mounted) setState(() {});\n    });\n  }\n\n  //显示高级重发\n  void showCustomRepeat(HttpRequest request) {\n    Navigator.of(context).pop();\n    Navigator.of(context).push(MaterialPageRoute(\n        builder: (context) => futureWidget(SharedPreferences.getInstance(),\n            (prefs) => MobileCustomRepeat(onRepeat: () => onRepeat(request), prefs: prefs))));\n  }\n\n  void onRepeat(HttpRequest request) {\n    var httpRequest = request.copy(uri: request.requestUrl);\n    var proxyInfo = widget.proxyServer.isRunning ? ProxyInfo.of(\"127.0.0.1\", widget.proxyServer.port) : null;\n    HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo);\n\n    if (mounted) {\n      FlutterToastr.show(localizations.reSendRequest, context);\n    }\n  }\n\n  //重命名\n  void rename(Favorite item) {\n    String? name = item.name;\n    showDialog(\n        context: context,\n        builder: (context) {\n          return AlertDialog(\n            content: TextFormField(\n              initialValue: name,\n              decoration: InputDecoration(label: Text(localizations.name)),\n              onChanged: (val) => name = val,\n            ),\n            actions: <Widget>[\n              TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n              TextButton(\n                child: Text(localizations.save),\n                onPressed: () {\n                  Navigator.maybePop(context);\n                  setState(() {\n                    item.name = name?.isEmpty == true ? null : name;\n                    FavoriteStorage.flushConfig();\n                  });\n                },\n              ),\n            ],\n          );\n        });\n  }\n\n  //点击事件\n  void onClick() {\n    Navigator.push(context, MaterialPageRoute(builder: (context) {\n      return NetworkTabController(\n          proxyServer: widget.proxyServer,\n          httpRequest: request,\n          httpResponse: request.response,\n          title: Text(localizations.captureDetail, style: const TextStyle(fontSize: 16)));\n    }));\n  }\n\n  Widget itemButton(\n      {required String label, required IconData icon, required Function() onPressed, double iconSize = 20}) {\n    var theme = Theme.of(context);\n    var style = theme.textTheme.bodyMedium;\n    return TextButton.icon(\n        onPressed: onPressed,\n        label: Text(label, style: style),\n        icon: Icon(icon, size: iconSize, color: theme.colorScheme.primary.withOpacity(0.65)));\n  }\n\n  Widget menuItem({required Widget left, required Widget right}) {\n    return Row(\n      children: [\n        SizedBox(width: 130, child: Align(alignment: Alignment.centerLeft, child: left)),\n        Expanded(child: Align(alignment: Alignment.centerLeft, child: right))\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/request/history.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:date_format/date_format.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/storage/histories.dart';\nimport 'package:proxypin/ui/component/history_cache_time.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/mobile/request/list.dart';\nimport 'package:proxypin/ui/mobile/request/search.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:share_plus/share_plus.dart';\n\nimport '../../../utils/har.dart';\n\nclass MobileHistory extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final HistoryTask historyTask;\n  final ListenableList<HttpRequest> container;\n\n  const MobileHistory({super.key, required this.proxyServer, required this.container, required this.historyTask});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _MobileHistoryState();\n  }\n}\n\n///重发所有请求\nvoid _repeatAllRequests(Iterable<HttpRequest> requests, ProxyServer proxyServer, {BuildContext? context}) async {\n  var localizations = context == null ? null : AppLocalizations.of(context);\n\n  for (var request in requests) {\n    var httpRequest = request.copy(uri: request.requestUrl);\n    var proxyInfo = proxyServer.isRunning ? ProxyInfo.of(\"127.0.0.1\", proxyServer.port) : null;\n    try {\n      await HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo, timeout: const Duration(seconds: 3));\n      if (context != null && context.mounted) {\n        FlutterToastr.show(localizations!.reSendRequest, rootNavigator: true, context);\n      }\n    } catch (e) {\n      if (context != null && context.mounted) {\n        FlutterToastr.show('${localizations!.fail} $e', rootNavigator: true, context);\n      }\n    }\n  }\n}\n\nclass _MobileHistoryState extends State<MobileHistory> {\n  ///是否保存会话\n  static bool _sessionSaved = false;\n  late Configuration configuration;\n  var storageInstance = HistoryStorage.instance;\n\n  @override\n  void initState() {\n    super.initState();\n    configuration = widget.proxyServer.configuration;\n  }\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return futureWidget(storageInstance, (storage) {\n      List<Widget> children = [];\n\n      if (widget.container.isNotEmpty == true && !_sessionSaved && widget.historyTask.history == null) {\n        //当前会话未保存，是否保存当前会话\n        children.add(buildSaveSession(storage));\n      }\n\n      var histories = storage.histories;\n      for (int i = histories.length - 1; i >= 0; i--) {\n        var entry = histories.elementAt(i);\n        children.add(buildItem(storage, i, entry));\n      }\n\n      return Scaffold(\n          appBar: AppBar(\n              title: Text(localizations.history, style: const TextStyle(fontSize: 16)),\n              centerTitle: true,\n              actions: [\n                IconButton(\n                    onPressed: () => import(storage),\n                    icon: const Icon(Icons.input, size: 18),\n                    tooltip: localizations.import),\n                const SizedBox(width: 3),\n                HistoryCacheTime(configuration, onSelected: (val) {\n                  if (val == 0) {\n                    widget.container.removeListener(widget.historyTask);\n                  } else {\n                    widget.container.addListener(widget.historyTask);\n                  }\n                }),\n                const SizedBox(width: 5)\n              ]),\n          body: children.isEmpty\n              ? Center(child: Text(localizations.emptyData))\n              : ListView.separated(\n                  itemCount: children.length,\n                  itemBuilder: (context, index) => children[index],\n                  separatorBuilder: (_, index) => const Divider(thickness: 0.3, height: 0),\n                ));\n    });\n  }\n\n  //构建保存会话\n  Widget buildSaveSession(HistoryStorage storage) {\n    var name = formatDate(DateTime.now(), [mm, '-', d, ' ', HH, ':', nn, ':', ss]);\n\n    return ListTile(\n        dense: true,\n        title: Text(name),\n        subtitle: Text(localizations.historyUnSave),\n        trailing: TextButton.icon(\n          icon: const Icon(Icons.save),\n          label: Text(localizations.save),\n          onPressed: () async {\n            setState(() {\n              widget.container.addListener(widget.historyTask);\n              widget.historyTask.startTask();\n              _sessionSaved = true;\n            });\n          },\n        ),\n        onTap: () {});\n  }\n\n  //导入har\n  import(HistoryStorage storage) async {\n    FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any);\n    if (result == null || result.files.isEmpty) {\n      return;\n    }\n\n    try {\n      var historyItem = await storage.addHarFile(result.files.single.xFile);\n      setState(() {\n        toRequestsView(historyItem, storage);\n        FlutterToastr.show(localizations.importSuccess, context);\n      });\n    } catch (e, t) {\n      logger.e(\"导入失败\", error: e, stackTrace: t);\n      if (mounted) {\n        FlutterToastr.show(\"${localizations.importFailed} $e\", context);\n      }\n    }\n  }\n\n  int selectIndex = -1;\n\n  //构建历史记录\n  Widget buildItem(HistoryStorage storage, int index, HistoryItem item) {\n    return GestureDetector(\n        onLongPressStart: (detail) async {\n          if (Platform.isAndroid) HapticFeedback.mediumImpact();\n          setState(() {\n            selectIndex = index;\n          });\n          showContextMenu(context, detail.globalPosition.translate(-50, index == 0 ? -100 : 100), items: [\n            PopupMenuItem(child: Text(localizations.rename), onTap: () => renameHistory(storage, item)),\n            PopupMenuItem(\n                child: Text(localizations.share), onTap: () => export(storage, item, offset: detail.globalPosition)),\n            const PopupMenuDivider(height: 0.3),\n            PopupMenuItem(\n                child: Text(localizations.repeatAllRequests),\n                onTap: () async {\n                  var requests = (await storage.getRequests(item)).reversed;\n                  //重发所有请求\n                  _repeatAllRequests(requests.toList(), widget.proxyServer, context: mounted ? context : null);\n                }),\n            const PopupMenuDivider(height: 0.3),\n            PopupMenuItem(child: Text(localizations.delete), onTap: () => deleteHistory(storage, index))\n          ]).whenComplete(() {\n            setState(() {\n              selectIndex = -1;\n            });\n          });\n        },\n        child: ListTile(\n          dense: true,\n          selected: selectIndex == index,\n          title: Text(item.name),\n          subtitle: Text(localizations.historySubtitle(item.requestLength, item.size)),\n          onTap: () => toRequestsView(item, storage),\n        ));\n  }\n\n  toRequestsView(HistoryItem item, HistoryStorage storage) {\n    Navigator.of(context)\n        .push(MaterialPageRoute(\n            builder: (BuildContext context) => HistoryRecord(history: item, proxyServer: widget.proxyServer)))\n        .then((value) async {\n      if (item != widget.historyTask.history && item.requests != null && item.requestLength != item.requests?.length) {\n        await storage.flushRequests(item, item.requests!);\n        setState(() {});\n      }\n      Future.delayed(const Duration(seconds: 60), () => item.requests = null);\n    });\n  }\n\n  //导出har\n  export(HistoryStorage storage, HistoryItem item, {Offset? offset}) async {\n    //文件名称\n    String fileName =\n        '${item.name.contains(\"ProxyPin\") ? '' : 'ProxyPin'}${item.name}.har'.replaceAll(\" \", \"_\").replaceAll(\":\", \"_\");\n    //获取请求\n    List<HttpRequest> requests = await storage.getRequests(item);\n    var json = await Har.writeJson(requests, title: item.name);\n    var file = XFile.fromData(utf8.encode(json), mimeType: \"har\");\n\n    Rect? rect;\n    if (await Platforms.isIpad() && offset != null) {\n      rect = Rect.fromCenter(center: offset, width: 1, height: 1);\n    }\n\n    Share.shareXFiles([file], fileNameOverrides: [fileName], sharePositionOrigin: rect);\n    Future.delayed(const Duration(seconds: 30), () => item.requests = null);\n  }\n\n  //重命名\n  renameHistory(HistoryStorage storage, HistoryItem item) {\n    String name = \"\";\n    showDialog(\n        context: context,\n        builder: (context) {\n          return AlertDialog(\n            content: TextField(\n              decoration: InputDecoration(label: Text(localizations.name)),\n              onChanged: (val) => name = val,\n            ),\n            actions: <Widget>[\n              TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n              TextButton(\n                child: Text(localizations.save),\n                onPressed: () {\n                  if (name.isEmpty) {\n                    FlutterToastr.show(localizations.historyEmptyName, context, position: 2);\n                    return;\n                  }\n                  Navigator.of(context).pop();\n                  setState(() {\n                    item.name = name;\n                    storage.refresh();\n                  });\n                },\n              ),\n            ],\n          );\n        });\n  }\n\n  //删除\n  deleteHistory(HistoryStorage storage, int index) {\n    showDialog(\n        context: context,\n        builder: (context) {\n          return AlertDialog(\n            title: Text(localizations.historyDeleteConfirm, style: const TextStyle(fontSize: 18)),\n            actions: [\n              TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n              TextButton(\n                  onPressed: () {\n                    setState(() {\n                      if (storage.getHistory(index) == widget.historyTask.history) {\n                        widget.historyTask.cancelTask();\n                      }\n                      storage.removeHistory(index);\n                    });\n                    FlutterToastr.show(localizations.deleteSuccess, context);\n                    Navigator.pop(context);\n                  },\n                  child: Text(localizations.delete)),\n            ],\n          );\n        });\n  }\n}\n\nclass HistoryRecord extends StatefulWidget {\n  final HistoryItem history;\n  final ProxyServer proxyServer;\n\n  const HistoryRecord({super.key, required this.history, required this.proxyServer});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _HistoryRecordState();\n  }\n}\n\nclass _HistoryRecordState extends State<HistoryRecord> {\n  GlobalKey<RequestListState> requestStateKey = GlobalKey<RequestListState>();\n\n  ///搜索key\n  final GlobalKey<MobileSearchState> searchStateKey = GlobalKey<MobileSearchState>();\n\n  var searchEnabled = ValueNotifier(false);\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void dispose() {\n    searchEnabled.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: PreferredSize(\n            preferredSize: const Size.fromHeight(38),\n            child: AppBar(\n              title: ValueListenableBuilder(\n                  valueListenable: searchEnabled,\n                  builder: (BuildContext context, bool value, Widget? child) {\n                    return value\n                        ? MobileSearch(\n                            key: searchStateKey,\n                            onSearch: (val) => requestStateKey.currentState?.search(val),\n                            showSearch: true)\n                        : Text(localizations.historyRecordTitle(widget.history.requestLength, widget.history.name),\n                            style: const TextStyle(fontSize: 16));\n                  }),\n              actions: [\n                PopupMenuButton(\n                    offset: const Offset(0, 30),\n                    icon: const Icon(Icons.more_vert_outlined),\n                    itemBuilder: (BuildContext context) {\n                      return [\n                        PopupMenuItem(\n                            onTap: () {\n                              if (searchEnabled.value) {\n                                searchStateKey.currentState?.showSearch();\n                                return;\n                              }\n                              searchEnabled.value = true;\n                            },\n                            child: IconText(icon: const Icon(Icons.search), text: localizations.search)),\n                        PopupMenuItem(\n                            onTap: () => export(context),\n                            child: IconText(icon: const Icon(Icons.share), text: localizations.viewExport)),\n                        PopupMenuItem(\n                            onTap: () async {\n                              var requests = requestStateKey.currentState?.currentView();\n                              if (requests == null) return;\n                              //重发所有请求\n                              _repeatAllRequests(requests.toList(), widget.proxyServer,\n                                  context: mounted ? context : null);\n                            },\n                            child: IconText(icon: const Icon(Icons.repeat), text: localizations.repeatAllRequests)),\n                      ];\n                    }),\n              ],\n            )),\n        body: futureWidget(\n            loading: true,\n            HistoryStorage.instance.then((storage) => storage.getRequests(widget.history)),\n            (data) =>\n                RequestListWidget(proxyServer: widget.proxyServer, list: ListenableList(data), key: requestStateKey)));\n  }\n\n  //导出har\n  export(BuildContext context) async {\n    var item = widget.history;\n    requestStateKey.currentState?.export(context, item.name);\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/request/list.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/channel/channel.dart';\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/ui/mobile/request/domians.dart';\nimport 'package:proxypin/ui/mobile/request/request_sequence.dart';\nimport 'package:proxypin/utils/har.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:share_plus/share_plus.dart';\n\nimport '../../component/model/search_model.dart';\n\n/// 请求列表\n/// @author wanghongen\nclass RequestListWidget extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final ListenableList<HttpRequest>? list;\n\n  const RequestListWidget({super.key, required this.proxyServer, this.list});\n\n  @override\n  State<StatefulWidget> createState() {\n    return RequestListState();\n  }\n}\n\nclass RequestListState extends State<RequestListWidget> {\n  final GlobalKey<RequestSequenceState> requestSequenceKey = GlobalKey<RequestSequenceState>();\n  final GlobalKey<DomainListState> domainListKey = GlobalKey<DomainListState>();\n\n  //请求列表容器\n  ListenableList<HttpRequest> container = ListenableList();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.list != null) {\n      container = widget.list!;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    List<Widget> tabs = [Tab(child: Text(localizations.sequence)), Tab(child: Text(localizations.domainList))];\n\n    //double click scroll to top\n    var tabClickHandles = [\n      DoubleClickHandle(handle: () => requestSequenceKey.currentState?.scrollToTop()),\n      DoubleClickHandle(handle: () => domainListKey.currentState?.scrollToTop())\n    ];\n\n    return DefaultTabController(\n        length: tabs.length,\n        child: Scaffold(\n          appBar: AppBar(\n              title: TabBar(tabs: tabs, onTap: (index) => tabClickHandles[index].call()),\n              automaticallyImplyLeading: false),\n          body: TabBarView(\n            children: [\n              RequestSequence(\n                  key: requestSequenceKey,\n                  container: container,\n                  proxyServer: widget.proxyServer,\n                  onRemove: sequenceRemove),\n              DomainList(\n                  key: domainListKey, list: container, proxyServer: widget.proxyServer, onRemove: domainListRemove),\n            ],\n          ),\n        ));\n  }\n\n  ///添加请求\n  add(Channel channel, HttpRequest request) {\n    container.add(request);\n    requestSequenceKey.currentState?.add(request);\n    domainListKey.currentState?.add(request);\n  }\n\n  ///添加响应\n  addResponse(ChannelContext channelContext, HttpResponse response) {\n    requestSequenceKey.currentState?.addResponse(response);\n    domainListKey.currentState?.addResponse(response);\n  }\n\n  ///移除\n  domainListRemove(List<HttpRequest> list) {\n    container.removeWhere((element) => list.contains(element));\n    requestSequenceKey.currentState?.remove(list);\n  }\n\n  ///全部请求删除\n  sequenceRemove(List<HttpRequest> list) {\n    container.removeWhere((element) => list.contains(element));\n    domainListKey.currentState?.remove(list);\n  }\n\n  search(SearchModel searchModel) {\n    requestSequenceKey.currentState?.search(searchModel);\n    domainListKey.currentState?.search(searchModel.keyword?.trim());\n  }\n\n  Iterable<HttpRequest>? currentView() {\n    return requestSequenceKey.currentState?.currentView();\n  }\n\n  ///清理\n  clean() {\n    setState(() {\n      container.clear();\n      domainListKey.currentState?.clean();\n      requestSequenceKey.currentState?.clean();\n    });\n  }\n\n  ///清理早期数据\n  cleanupEarlyData(int retain) {\n    var list = container.source;\n    if (list.length <= retain) {\n      return;\n    }\n\n    container.removeRange(0, list.length - retain);\n\n    domainListKey.currentState?.clean();\n    requestSequenceKey.currentState?.clean();\n  }\n\n  //导出har\n  export(BuildContext context, String title) async {\n    //文件名称\n    String fileName =\n        '${title.contains(\"ProxyPin\") ? '' : 'ProxyPin'}$title.har'.replaceAll(\" \", \"_\").replaceAll(\":\", \"_\");\n    //获取请求\n    var view = currentView()!;\n    var json = await Har.writeJson(view.toList(), title: title);\n    var file = XFile.fromData(utf8.encode(json), name: fileName, mimeType: \"har\");\n\n    RenderBox? box;\n    if (await Platforms.isIpad() && context.mounted) {\n      box = context.findRenderObject() as RenderBox?;\n    }\n    Share.shareXFiles([file],\n        fileNameOverrides: [fileName],\n        sharePositionOrigin: box == null ? null : box.localToGlobal(Offset.zero) & box.size);\n  }\n\n  sort(bool sortDesc) {\n    requestSequenceKey.currentState?.sort(sortDesc);\n    domainListKey.currentState?.sort(sortDesc);\n  }\n\n}\n\nclass DoubleClickHandle {\n  int tabClickTime = 0;\n  final Function()? handle;\n\n  DoubleClickHandle({this.handle});\n\n  void call() {\n    if (handle == null) {\n      return;\n    }\n\n    if (DateTime.now().millisecondsSinceEpoch - tabClickTime < 500) {\n      handle?.call();\n    }\n    tabClickTime = DateTime.now().millisecondsSinceEpoch;\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/request/repeat.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:math';\n\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\n///高级重放\n/// @author wang\nclass MobileCustomRepeat extends StatefulWidget {\n  final Function onRepeat;\n  final SharedPreferences prefs;\n\n  const MobileCustomRepeat({super.key, required this.onRepeat, required this.prefs});\n\n  @override\n  State<StatefulWidget> createState() => _CustomRepeatState();\n}\n\nclass _CustomRepeatState extends State<MobileCustomRepeat> {\n  TextEditingController count = TextEditingController(text: '1');\n  TextEditingController interval = TextEditingController(text: '0');\n  TextEditingController minInterval = TextEditingController(text: '0');\n  TextEditingController maxInterval = TextEditingController(text: '1000');\n  TextEditingController delay = TextEditingController(text: '0');\n\n  bool fixed = true;\n  bool keepSetting = true;\n\n  DateTime? time;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n\n    var customerRepeat = widget.prefs.getString('customerRepeat');\n    keepSetting = customerRepeat != null;\n    if (customerRepeat != null) {\n      Map<String, dynamic> data = jsonDecode(customerRepeat);\n      count.text = data['count'];\n      interval.text = data['interval'];\n      minInterval.text = data['minInterval'];\n      maxInterval.text = data['maxInterval'];\n      delay.text = data['delay'];\n      fixed = data['fixed'] == true;\n    }\n  }\n\n  @override\n  void dispose() {\n    count.dispose();\n    interval.dispose();\n    delay.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final formKey = GlobalKey<FormState>();\n\n    return Scaffold(\n        appBar: AppBar(\n          title: Text(localizations.customRepeat, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n          actions: [\n            TextButton(\n              child: Text(localizations.done),\n              onPressed: () {\n                if (!formKey.currentState!.validate()) {\n                  return;\n                }\n                if (keepSetting) {\n                  widget.prefs.setString(\n                      'customerRepeat',\n                      jsonEncode({\n                        'count': count.text,\n                        'interval': interval.text,\n                        'minInterval': minInterval.text,\n                        'maxInterval': maxInterval.text,\n                        'delay': delay.text,\n                        'fixed': fixed\n                      }));\n                } else {\n                  widget.prefs.remove('customerRepeat');\n                }\n\n                int delayValue = int.parse(delay.text);\n                if (time != null) {\n                  DateTime now = DateTime.now();\n                  DateTime schedule = DateTime(now.year, now.month, now.day, time!.hour, time!.minute);\n                  if (schedule.isBefore(now)) {\n                    schedule = schedule.add(const Duration(days: 1));\n                  }\n                  delayValue += schedule.difference(now).inMilliseconds;\n                }\n\n                Future.delayed(Duration(milliseconds: delayValue), () => submitTask(int.parse(count.text)));\n                Navigator.of(context).pop();\n              },\n            )\n          ],\n        ),\n        body: SingleChildScrollView(\n            padding: const EdgeInsets.all(15),\n            child: Form(\n              key: formKey,\n              child: Column(\n                children: <Widget>[\n                  field(localizations.repeatCount, textField(count)), //次数\n                  const SizedBox(height: 6),\n                  intervalWidget(), //间隔\n                  const SizedBox(height: 6),\n                  field(localizations.repeatDelay, textField(delay)), //延时\n                  const SizedBox(height: 6),\n                  field(\n                      localizations.scheduleTime,\n                      InkWell(\n                          onTap: _pickScheduleDateTime,\n                          child: Container(\n                            height: 42,\n                            padding: const EdgeInsets.only(left: 10, right: 10),\n                            decoration: BoxDecoration(\n                                border: Border.all(\n                                    color: Theme.of(context).colorScheme.primary.withAlpha((0.5 * 255).round()),\n                                    width: 1.0),\n                                borderRadius: BorderRadius.circular(4)),\n                            child: Row(\n                              children: [\n                                Text(time == null\n                                    ? ''\n                                    : \"${time!.year}-${_two(time!.month)}-${_two(time!.day)} ${_two(time!.hour)}:${_two(time!.minute)}\"),\n                                const Expanded(child: SizedBox()),\n                                if (time != null)\n                                  InkWell(\n                                    onTap: () {\n                                      setState(() {\n                                        time = null;\n                                      });\n                                    },\n                                    child: const Icon(Icons.clear, size: 18),\n                                  ),\n                                if (time == null)\n                                  Icon(Icons.access_time, size: 18, color: Theme.of(context).colorScheme.primary),\n                              ],\n                            ),\n                          ))), //指定时间\n                  const SizedBox(height: 6),\n                  //记录选择\n                  Row(children: [\n                    Text(localizations.keepCustomSettings),\n                    Expanded(\n                        child: Checkbox(\n                            value: keepSetting,\n                            onChanged: (val) {\n                              setState(() {\n                                keepSetting = val == true;\n                              });\n                            })),\n                  ])\n                ],\n              ),\n            )));\n  }\n\n  String _two(int v) => v.toString().padLeft(2, '0');\n\n  //定时重放\n  void submitTask(int counter) {\n    if (counter <= 0) {\n      return;\n    }\n    widget.onRepeat.call();\n\n    int intervalValue = int.parse(interval.text);\n    //随机\n    if (!fixed) {\n      int min = int.parse(minInterval.text);\n      int max = int.parse(maxInterval.text);\n      intervalValue = Random().nextInt(max - min) + min;\n    }\n\n    Future.delayed(Duration(milliseconds: intervalValue), () {\n      if (counter - 1 > 0) {\n        submitTask(counter - 1);\n      }\n    });\n  }\n\n  //间隔widget\n  Widget intervalWidget() {\n    return Row(\n      children: [\n        SizedBox(width: 83, child: Text(localizations.repeatInterval)),\n        Expanded(\n            child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n          //Checkbox样式 固定和随机\n          Row(children: [\n            SizedBox(\n                width: 112,\n                height: 35,\n                child: Transform.scale(\n                    scale: 0.82,\n                    child: CheckboxListTile(\n                        contentPadding: EdgeInsets.zero,\n                        title: Text(\"${localizations.fixed}:\"),\n                        value: fixed,\n                        dense: true,\n                        onChanged: (val) {\n                          setState(() {\n                            fixed = true;\n                          });\n                        }))),\n            Expanded(child: textField(interval, style: const TextStyle(fontSize: 13))),\n          ]),\n          const SizedBox(height: 5),\n          Row(children: [\n            SizedBox(\n                width: 112,\n                child: Transform.scale(\n                    scale: 0.82,\n                    child: CheckboxListTile(\n                        contentPadding: EdgeInsets.zero,\n                        title: Text(\"${localizations.random}:\"),\n                        value: !fixed,\n                        dense: true,\n                        onChanged: (val) {\n                          setState(() {\n                            fixed = false;\n                          });\n                        }))),\n            Flexible(child: textField(minInterval, style: const TextStyle(fontSize: 13))),\n            const Padding(padding: EdgeInsets.symmetric(horizontal: 5), child: Text(\"-\")),\n            Flexible(child: textField(maxInterval, style: const TextStyle(fontSize: 13))),\n          ]),\n        ])),\n      ],\n    );\n  }\n\n\n  Future<void> _pickScheduleDateTime() async {\n    DateTime now = DateTime.now();\n    DateTime temp = time ?? now;\n    if (temp.isBefore(now)) {\n      temp = now;\n    }\n\n    DateTime? selected = await showModalBottomSheet<DateTime>(\n      context: context,\n      builder: (BuildContext context) {\n        DateTime current = temp;\n        return SafeArea(\n          child: SizedBox(\n            height: 300,\n            child: Column(\n              children: [\n                Expanded(\n                  child: CupertinoDatePicker(\n                    mode: CupertinoDatePickerMode.dateAndTime,\n                    use24hFormat: true,\n                    initialDateTime: temp,\n                    minimumDate: now,\n                    maximumDate: now.add(const Duration(days: 365)),\n                    onDateTimeChanged: (DateTime value) {\n                      current = value;\n                    },\n                  ),\n                ),\n                Row(\n                  mainAxisAlignment: MainAxisAlignment.end,\n                  children: [\n                    TextButton(\n                      onPressed: () => Navigator.pop(context),\n                      child: Text(localizations.cancel),\n                    ),\n                    TextButton(\n                      onPressed: () => Navigator.pop(context, current),\n                      child: Text(localizations.done),\n                    ),\n                  ],\n                )\n              ],\n            ),\n          ),\n        );\n      },\n    );\n\n    if (selected != null) {\n      setState(() {\n        time = selected;\n      });\n    }\n  }\n\n  Widget field(String label, Widget child) {\n    return Row(\n      children: [\n        SizedBox(width: 95, child: Text(\"$label :\")),\n        Expanded(child: child),\n      ],\n    );\n  }\n\n  FormField textField(TextEditingController? controller, {TextStyle? style}) {\n    Color color = Theme.of(context).colorScheme.primary;\n\n    return TextFormField(\n      controller: controller,\n      keyboardType: TextInputType.number,\n      inputFormatters: [FilteringTextInputFormatter.digitsOnly],\n      style: style,\n      decoration: InputDecoration(\n          errorStyle: const TextStyle(height: 2, fontSize: 0),\n          contentPadding: const EdgeInsets.only(left: 10, right: 10, top: 5, bottom: 5),\n          border: OutlineInputBorder(borderSide: BorderSide(width: 1, color: color.withOpacity(0.3))),\n          enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.5, color: color.withOpacity(0.5))),\n          focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color))),\n      validator: (val) => val == null || val.isEmpty ? localizations.cannotBeEmpty : null,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/request/request.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:io';\n\nimport 'package:date_format/date_format.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/network/components/manager/script_manager.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/network/util/cache.dart';\nimport 'package:proxypin/storage/favorites.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/content/panel.dart';\nimport 'package:proxypin/ui/mobile/request/repeat.dart';\nimport 'package:proxypin/ui/mobile/request/request_editor.dart';\nimport 'package:proxypin/ui/mobile/setting/request_rewrite.dart';\nimport 'package:proxypin/ui/mobile/setting/script.dart';\nimport 'package:proxypin/utils/curl.dart';\nimport 'package:proxypin/utils/keyword_highlight.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/navigator.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\n///请求行\nclass RequestRow extends StatefulWidget {\n  final int index;\n  final HttpRequest request;\n  final ProxyServer proxyServer;\n  final bool displayDomain;\n  final Function(HttpRequest)? onRemove;\n\n  const RequestRow(\n      {super.key,\n      required this.request,\n      required this.proxyServer,\n      this.displayDomain = true,\n      this.onRemove,\n      required this.index});\n\n  @override\n  State<StatefulWidget> createState() {\n    return RequestRowState();\n  }\n}\n\nclass RequestRowState extends State<RequestRow> {\n  static ExpiringCache<String, Image> imageCache = ExpiringCache<String, Image>(const Duration(minutes: 5));\n  static Set<String> autoReadRequests = <String>{};\n\n  late HttpRequest request;\n  HttpResponse? response;\n  bool selected = false;\n  Color? highlightColor; //高亮颜色\n\n  AppLocalizations get localizations => AppLocalizations.of(availableContext)!;\n\n  void change(HttpResponse response) {\n    setState(() {\n      this.response = response;\n    });\n  }\n\n  @override\n  void initState() {\n    request = widget.request;\n    response = request.response;\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    autoReadRequests.remove(widget.request.requestId);\n    super.dispose();\n  }\n\n  Color? color(String url) {\n    if (highlightColor != null) {\n      return highlightColor;\n    }\n\n    highlightColor = KeywordHighlights.getHighlightColor(url);\n    if (highlightColor != null) {\n      return highlightColor;\n    }\n\n    return autoReadRequests.contains(request.requestId) ? Colors.grey : null;\n  }\n\n  BuildContext getContext() => mounted ? super.context : NavigatorHelper().context;\n\n  BuildContext get availableContext => getContext();\n\n  @override\n  Widget build(BuildContext context) {\n    String url = widget.displayDomain ? request.requestUrl : request.path;\n\n    var title = Strings.autoLineString('${request.method.name} $url');\n\n    var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]);\n    var contentType = response?.contentType.name.toUpperCase() ?? '';\n    var packagesSize = getPackagesSize(request, response);\n\n    var subTitle = '$time - [${response?.status.code ?? ''}] $contentType $packagesSize ${response?.costTime() ?? ''}';\n\n    var highlightColor = color(url);\n\n    return GestureDetector(\n        onLongPressStart: menu,\n        child: ListTile(\n          visualDensity: const VisualDensity(vertical: -4),\n          minLeadingWidth: 5,\n          selected: selected,\n          textColor: highlightColor,\n          selectedColor: highlightColor,\n          leading: appIcon(),\n          title: Text(title.fixAutoLines(),\n              overflow: TextOverflow.ellipsis, maxLines: 2, style: const TextStyle(fontSize: 14)),\n          subtitle: Text.rich(\n              maxLines: 1,\n              TextSpan(children: [\n                TextSpan(text: '#${widget.index} ', style: const TextStyle(fontSize: 11, color: Colors.teal)),\n                TextSpan(text: subTitle, style: const TextStyle(fontSize: 11, color: Colors.grey)),\n              ])),\n          trailing: getIcon(response, color: highlightColor),\n          contentPadding:\n              Platform.isIOS ? const EdgeInsets.symmetric(horizontal: 8) : const EdgeInsets.only(left: 3, right: 5),\n          onTap: () {\n            if (AppConfiguration.current?.autoReadEnabled == true) {\n              if (autoReadRequests.add(request.requestId)) {\n                setState(() {});\n              }\n            }\n\n            Navigator.of(getContext()).push(MaterialPageRoute(builder: (context) {\n              return NetworkTabController(\n                  proxyServer: widget.proxyServer,\n                  httpRequest: request,\n                  httpResponse: response,\n                  title: Text(localizations.captureDetail, style: const TextStyle(fontSize: 16)));\n            }));\n          },\n        ));\n  }\n\n  Widget? appIcon() {\n    if (Platform.isIOS) {\n      return null;\n    }\n    if (request.processInfo == null) {\n      return const Icon(Icons.question_mark, size: 38);\n    }\n\n    //如果有缓存图标直接返回图标\n    if (request.processInfo!.hasCacheIcon) {\n      return imageCache.putIfAbsent(request.processInfo!.id, () {\n        return Image.memory(request.processInfo!.cacheIcon!, width: 40, gaplessPlayback: true);\n      });\n    }\n\n    return FutureBuilder(\n        future: request.processInfo!.getIcon(),\n        builder: (BuildContext context, AsyncSnapshot<Uint8List> snapshot) {\n          if (snapshot.hasData) {\n            return Image.memory(snapshot.data!, width: 40);\n          }\n          return const SizedBox(width: 40);\n        });\n  }\n\n  ///菜单\n  void menu(details) {\n    setState(() {\n      selected = true;\n    });\n\n    var globalPosition = details.globalPosition;\n    MediaQueryData mediaQuery = MediaQuery.of(context);\n    var position = RelativeRect.fromLTRB(globalPosition.dx, globalPosition.dy, globalPosition.dx, globalPosition.dy);\n    // Trigger haptic feedback\n    if (Platform.isAndroid) HapticFeedback.mediumImpact();\n\n    showMenu(\n        context: context,\n        constraints: BoxConstraints(maxWidth: mediaQuery.size.width * 0.88),\n        position: position,\n        items: [\n          //复制url\n          PopupMenuContainer(\n              child: Column(\n            children: [\n              Align(\n                alignment: Alignment.centerLeft,\n                child: Padding(\n                    padding: EdgeInsets.only(left: 20, top: 5),\n                    child: Text(localizations.selectAction, style: Theme.of(context).textTheme.bodyLarge)),\n              ),\n              //copy\n              menuItem(\n                left: itemButton(\n                    onPressed: () {\n                      Clipboard.setData(ClipboardData(text: request.requestUrl)).then((value) {\n                        FlutterToastr.show(localizations.copied, getContext());\n                        Navigator.maybePop(getContext());\n                      });\n                    },\n                    label: localizations.copyUrl,\n                    icon: Icons.link,\n                    iconSize: 22),\n                right: itemButton(\n                    onPressed: () {\n                      Clipboard.setData(ClipboardData(text: curlRequest(request))).then((value) {\n                        FlutterToastr.show(localizations.copied, getContext());\n                        Navigator.maybePop(getContext());\n                      });\n                    },\n                    label: localizations.copyCurl,\n                    icon: Icons.code),\n              ),\n              //repeat\n              menuItem(\n                left: itemButton(\n                    onPressed: () {\n                      onRepeat(request);\n                      Navigator.maybePop(getContext());\n                    },\n                    label: localizations.repeat,\n                    icon: Icons.repeat_one),\n                right: itemButton(\n                    onPressed: () => showCustomRepeat(request), label: localizations.customRepeat, icon: Icons.repeat),\n              ),\n              //favorite and edit\n              menuItem(\n                left: itemButton(\n                    onPressed: () {\n                      FavoriteStorage.addFavorite(widget.request);\n                      FlutterToastr.show(localizations.addSuccess, availableContext);\n                      Navigator.maybePop(availableContext);\n                    },\n                    label: localizations.favorite,\n                    icon: Icons.favorite_outline),\n                right: itemButton(\n                    onPressed: () async {\n                      await Navigator.maybePop(availableContext);\n\n                      var pageRoute = MaterialPageRoute(\n                          builder: (context) =>\n                              MobileRequestEditor(request: widget.request, proxyServer: widget.proxyServer));\n                      Navigator.push(getContext(), pageRoute);\n                    },\n                    label: localizations.editRequest,\n                    icon: Icons.replay_outlined),\n              ),\n              //script and rewrite\n              menuItem(\n                left: itemButton(\n                    onPressed: () async {\n                      Navigator.maybePop(availableContext);\n\n                      var scriptManager = await ScriptManager.instance;\n                      var url = request.domainPath;\n                      var scriptItem = scriptManager.list.firstWhereOrNull((it) => it.urls.contains(url));\n                      String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem);\n\n                      var pageRoute = MaterialPageRoute(\n                          builder: (context) => ScriptEdit(\n                            scriptItem: scriptItem,\n                            script: script,\n                            urls: scriptItem?.urls ?? [url],\n                            title: request.hostAndPort?.host));\n\n                      Navigator.push(getContext(), pageRoute);\n                    },\n                    label: localizations.script,\n                    icon: Icons.javascript_outlined),\n                right: itemButton(\n                    onPressed: () async {\n                      Navigator.maybePop(availableContext);\n                      bool isRequest = response == null;\n                      var requestRewrites = await RequestRewriteManager.instance;\n\n                      var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace;\n                      var rule = requestRewrites.getRequestRewriteRule(request, ruleType);\n\n                      var rewriteItems = await requestRewrites.getRewriteItems(rule);\n\n                      var pageRoute = MaterialPageRoute(\n                          builder: (_) => RewriteRule(rule: rule, items: rewriteItems, request: request));\n                      var context = availableContext;\n                      if (context.mounted) Navigator.push(context, pageRoute);\n                    },\n                    label: localizations.requestRewrite,\n                    icon: Icons.edit_outlined),\n              ),\n              menuItem(\n                left: itemButton(\n                    onPressed: () {\n                      highlightColor = Theme.of(availableContext).colorScheme.primary;\n                      Navigator.maybePop(availableContext);\n                    },\n                    label: localizations.highlight,\n                    icon: Icons.highlight_outlined),\n                right: itemButton(\n                    onPressed: () {\n                      AppConfiguration.current?.autoReadEnabled = !AppConfiguration.current!.autoReadEnabled;\n                      highlightColor = Colors.grey;\n                      Navigator.maybePop(availableContext);\n                    },\n                    label: localizations.autoRead,\n                    icon: AppConfiguration.current?.autoReadEnabled == true\n                        ? Icons.check_box_outlined\n                        : Icons.check_box_outline_blank_outlined),\n              ),\n              SizedBox(height: 2),\n              Row(mainAxisAlignment: MainAxisAlignment.center, children: [\n                itemButton(\n                    onPressed: () {\n                      widget.onRemove?.call(request);\n                      FlutterToastr.show(localizations.deleteSuccess, availableContext);\n                      Navigator.maybePop(availableContext);\n                    },\n                    label: localizations.delete,\n                    icon: Icons.delete_outline),\n                SizedBox(width: 15),\n              ]),\n            ],\n          )),\n        ]).then((value) {\n      selected = false;\n      if (mounted) setState(() {});\n    });\n  }\n\n  //显示高级重发\n  Future<void> showCustomRepeat(HttpRequest request) async {\n    await Navigator.maybePop(availableContext);\n    var pageRoute = MaterialPageRoute(\n        builder: (context) => futureWidget(SharedPreferences.getInstance(),\n            (prefs) => MobileCustomRepeat(onRepeat: () => onRepeat(request), prefs: prefs)));\n\n    Navigator.push(getContext(), pageRoute);\n  }\n\n  void onRepeat(HttpRequest request) {\n    var httpRequest = request.copy(uri: request.requestUrl);\n    var proxyInfo = widget.proxyServer.isRunning ? ProxyInfo.of(\"127.0.0.1\", widget.proxyServer.port) : null;\n    HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo);\n\n    if (mounted) {\n      FlutterToastr.show(localizations.reSendRequest, context);\n    }\n  }\n\n  Widget itemButton(\n      {required String label, required IconData icon, required Function() onPressed, double iconSize = 20}) {\n    var theme = Theme.of(context);\n    var style = theme.textTheme.bodyMedium;\n    return TextButton.icon(\n        onPressed: onPressed,\n        label: Text(label, style: style),\n        icon: Icon(icon, size: iconSize, color: theme.colorScheme.primary.withOpacity(0.65)));\n  }\n\n  Widget menuItem({required Widget left, required Widget right}) {\n    return Row(\n      children: [\n        SizedBox(width: 130, child: Align(alignment: Alignment.centerLeft, child: left)),\n        Expanded(child: Align(alignment: Alignment.centerLeft, child: right))\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/request/request_editor.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/content/body.dart';\nimport 'package:proxypin/utils/curl.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nimport 'package:proxypin/ui/mobile/request/request_editor_source.dart';\n\nimport '../../component/http_method_popup.dart';\n\n/// @author wanghongen\nclass MobileRequestEditor extends StatefulWidget {\n  final HttpRequest? request;\n  final ProxyServer? proxyServer;\n  final RequestEditorSource source;\n  final Function(HttpRequest? request)? onExecuteRequest;\n  final Function(HttpResponse? response)? onExecuteResponse;\n  final HttpResponse? response;\n\n  const MobileRequestEditor({\n    super.key,\n    this.request,\n    this.response,\n    required this.proxyServer,\n    this.source = RequestEditorSource.editor,\n    this.onExecuteRequest,\n    this.onExecuteResponse,\n  });\n\n  @override\n  State<StatefulWidget> createState() {\n    return RequestEditorState();\n  }\n}\n\nclass RequestEditorState extends State<MobileRequestEditor> with SingleTickerProviderStateMixin {\n  final UrlQueryNotifier _queryNotifier = UrlQueryNotifier();\n  final requestLineKey = GlobalKey<_RequestLineState>();\n  final requestKey = GlobalKey<_HttpState>();\n  final responseKey = GlobalKey<_HttpState>();\n\n  ValueNotifier<int> responseChange = ValueNotifier<int>(-1);\n\n  late TabController tabController;\n\n  HttpRequest? request;\n  HttpResponse? response;\n\n  bool executed = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  var tabs = const [\n    Tab(text: \"请求\"),\n    Tab(text: \"响应\"),\n  ];\n\n  @override\n  void dispose() {\n    if ((widget.source == RequestEditorSource.breakpointRequest ||\n            widget.source == RequestEditorSource.breakpointResponse) &&\n        !executed) {\n      if (widget.source == RequestEditorSource.breakpointRequest) {\n        widget.onExecuteRequest?.call(null);\n      } else {\n        widget.onExecuteResponse?.call(null);\n      }\n    }\n\n    tabController.dispose();\n    responseChange.dispose();\n    _expanded.clear();\n    super.dispose();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    tabController = TabController(\n        length: tabs.length,\n        vsync: this,\n        initialIndex: widget.source == RequestEditorSource.breakpointResponse ? 1 : 0);\n    request = widget.request;\n    response = widget.response;\n    if (widget.request == null) {\n      curlParse();\n    }\n  }\n\n  Future<void> curlParse() async {\n    //获取剪切板内容\n    var data = await Clipboard.getData('text/plain');\n    if (data == null || data.text == null) {\n      return;\n    }\n    var text = data.text;\n    if (text?.startsWith(\"http://\") == true || text?.startsWith(\"https://\") == true) {\n      requestLineKey.currentState?.requestUrl.text = text!;\n      return;\n    }\n    if (text?.trimLeft().startsWith('curl') == true && mounted) {\n      showDialog(\n        context: context,\n        builder: (BuildContext context) {\n          return AlertDialog(\n              title: Text(localizations.prompt),\n              content: Text(localizations.curlSchemeRequest),\n              actions: [\n                TextButton(child: Text(localizations.cancel), onPressed: () => Navigator.of(context).pop()),\n                TextButton(\n                    child: Text(localizations.confirm),\n                    onPressed: () {\n                      try {\n                        setState(() {\n                          request = Curl.parse(text!);\n                          requestKey.currentState?.change(request!);\n                          requestLineKey.currentState?.change(request?.requestUrl, request?.method);\n                        });\n                      } catch (e) {\n                        FlutterToastr.show(localizations.fail, context);\n                      }\n                      Navigator.of(context).pop();\n                    }),\n              ]);\n        },\n      );\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n    if (!isCN) {\n      tabs = [\n        Tab(text: localizations.request),\n        Tab(text: localizations.response),\n      ];\n    }\n\n    var buttonText = localizations.send;\n    IconData icon = Icons.send;\n    if (widget.source == RequestEditorSource.breakpointRequest ||\n        widget.source == RequestEditorSource.breakpointResponse) {\n      buttonText = localizations.execute;\n      icon = Icons.play_arrow;\n    }\n\n    return Scaffold(\n        appBar: AppBar(\n            title: Text(localizations.httpRequest, style: const TextStyle(fontSize: 16)),\n            centerTitle: true,\n            leadingWidth: 72,\n            leading: TextButton(\n                onPressed: () => Navigator.pop(context),\n                child: Text(localizations.cancel, style: Theme.of(context).textTheme.bodyMedium)),\n            actions: [\n              TextButton.icon(\n                  icon: Icon(icon),\n                  label: Text(buttonText),\n                  onPressed: () {\n                    if (widget.source == RequestEditorSource.editor) {\n                      sendRequest();\n                    } else {\n                      executeBreakpoint();\n                    }\n                  })\n            ],\n            bottom: TabBar(controller: tabController, tabs: tabs)),\n        body: GestureDetector(\n            onTap: () => FocusManager.instance.primaryFocus?.unfocus(),\n            child: TabBarView(\n              controller: tabController,\n              children: [\n                _HttpWidget(\n                  title: _RequestLine(request: request, key: requestLineKey, urlQueryNotifier: _queryNotifier),\n                  message: request,\n                  key: requestKey,\n                  urlQueryNotifier: _queryNotifier,\n                  readOnly: widget.source == RequestEditorSource.breakpointResponse,\n                ),\n                ValueListenableBuilder(\n                    valueListenable: responseChange,\n                    builder: (_, value, __) {\n                      if (value == 0) {\n                        return const Center(child: CircularProgressIndicator());\n                      }\n\n                      return _HttpWidget(\n                          key: responseKey,\n                          title: Row(children: [\n                            Text(response?.protocolVersion ?? '',\n                                style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)),\n                            const SizedBox(width: 10),\n                            Text(\"${localizations.statusCode}: \", style: const TextStyle(fontWeight: FontWeight.w500)),\n                            const SizedBox(width: 10),\n                            Text(response?.status.toString() ?? \"\",\n                                style: TextStyle(\n                                    color: response?.status.isSuccessful() == true ? Colors.blue : Colors.red))\n                          ]),\n                          readOnly: widget.source != RequestEditorSource.breakpointResponse,\n                          message: response);\n                    }),\n              ],\n            )));\n  }\n\n  ///发送请求\n  sendRequest() async {\n    var currentState = requestLineKey.currentState!;\n    var headers = requestKey.currentState?.getHeaders();\n    var requestBody = requestKey.currentState?.getBody();\n    String url = currentState.requestUrl.text;\n\n    HttpRequest request = HttpRequest(currentState.requestMethod, Uri.parse(url).toString(),\n        protocolVersion: this.request?.protocolVersion ?? \"HTTP/1.1\");\n\n    request.headers.addAll(headers);\n    request.body = requestBody == null ? null : utf8.encode(requestBody);\n\n    var proxyInfo = widget.proxyServer?.isRunning == true ? ProxyInfo.of(\"127.0.0.1\", widget.proxyServer?.port) : null;\n\n    responseKey.currentState?.change(null);\n    responseChange.value = 0;\n\n    HttpClients.proxyRequest(proxyInfo: proxyInfo, request, timeout: Duration(seconds: 15)).then((response) {\n      this.response = response;\n      this.response?.request = request;\n      responseKey.currentState?.change(response);\n      responseChange.value = 1;\n\n      // FlutterToastr.show(localizations.requestSuccess, context);\n    }).catchError((e) {\n      responseChange.value = -1;\n      FlutterToastr.show('${localizations.fail}$e', context);\n    });\n\n    tabController.animateTo(1);\n  }\n\n  void executeBreakpoint() {\n    executed = true;\n    if (widget.source == RequestEditorSource.breakpointRequest) {\n      var currentState = requestLineKey.currentState!;\n      var headers = requestKey.currentState?.getHeaders();\n      var requestBody = requestKey.currentState?.getBody();\n      String url = currentState.requestUrl.text;\n\n      HttpRequest newRequest = request!.copy(uri: url);\n      newRequest.method = currentState.requestMethod;\n      newRequest.headers.clear();\n      newRequest.headers.addAll(headers);\n      newRequest.body = requestBody == null ? null : utf8.encode(requestBody);\n      widget.onExecuteRequest?.call(newRequest);\n    } else if (widget.source == RequestEditorSource.breakpointResponse) {\n      var headers = responseKey.currentState?.getHeaders();\n      var responseBody = responseKey.currentState?.getBody();\n\n      if (response == null) return;\n      HttpResponse newResponse = response!.copy();\n      newResponse.headers.clear();\n      newResponse.headers.addAll(headers);\n      newResponse.body = responseBody == null ? null : utf8.encode(responseBody);\n      widget.onExecuteResponse?.call(newResponse);\n    }\n  }\n}\n\ntypedef ParamCallback = void Function(String param);\n\nclass UrlQueryNotifier {\n  ParamCallback? _urlNotifier;\n  ParamCallback? _paramNotifier;\n\n  ParamCallback urlListener(ParamCallback listener) => _urlNotifier = listener;\n\n  ParamCallback paramListener(ParamCallback listener) => _paramNotifier = listener;\n\n  void onUrlChange(String url) => _urlNotifier?.call(url);\n\n  void onParamChange(String param) => _paramNotifier?.call(param);\n}\n\nclass _HttpWidget extends StatefulWidget {\n  final HttpMessage? message;\n  final bool readOnly;\n  final Widget title;\n  final UrlQueryNotifier? urlQueryNotifier;\n\n  const _HttpWidget({this.message, this.readOnly = false, super.key, required this.title, this.urlQueryNotifier});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _HttpState();\n  }\n}\n\nclass _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {\n  final headerKey = GlobalKey<KeyValState>();\n  Map<String, List<String>> initHeader = {};\n  HttpMessage? message;\n  String? body;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  String? getBody() {\n    return body;\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    message = widget.message;\n    body = widget.message?.bodyAsString;\n    if (widget.message?.headers == null && !widget.readOnly) {\n      initHeader[\"User-Agent\"] = [\"ProxyPin/${AppConfiguration.version}\"];\n      initHeader[\"Accept\"] = [\"*/*\"];\n      return;\n    }\n  }\n\n  void change(HttpMessage? message) {\n    this.message = message;\n    body = message?.bodyAsString;\n    headerKey.currentState?.refreshParam(message?.headers.getHeaders());\n    setState(() {});\n  }\n\n  HttpHeaders? getHeaders() {\n    return HttpHeaders.fromJson(headerKey.currentState?.getParams() ?? {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    if (message == null && widget.readOnly) {\n      return Center(child: Text(localizations.emptyData));\n    }\n\n    return SingleChildScrollView(\n        padding: const EdgeInsets.all(15),\n        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n          widget.title,\n          if (widget.urlQueryNotifier != null)\n            KeyValWidget(\n              title: 'URL${localizations.param}',\n              paramNotifier: widget.urlQueryNotifier,\n              params: message is HttpRequest ? (message as HttpRequest).requestUri?.queryParametersAll : null,\n              expanded: false,\n            ),\n          KeyValWidget(\n              title: \"Headers\",\n              params: message?.headers.getHeaders() ?? initHeader,\n              key: headerKey,\n              suggestions: HttpHeaders.commonHeaderKeys,\n              readOnly: widget.readOnly),\n          // 请求头\n          const SizedBox(height: 10),\n          const Text(\"Body\", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)),\n          _body(),\n          const SizedBox(height: 10),\n        ]));\n  }\n\n  Widget _body() {\n    if (widget.readOnly) {\n      return SingleChildScrollView(child: HttpBodyWidget(httpMessage: message));\n    }\n\n    return TextField(\n        controller: TextEditingController(text: body),\n        readOnly: widget.readOnly,\n        onChanged: (value) => body = value,\n        minLines: 3,\n        maxLines: 15);\n  }\n}\n\nclass _RequestLine extends StatefulWidget {\n  final HttpRequest? request;\n  final UrlQueryNotifier? urlQueryNotifier;\n\n  const _RequestLine({this.request, super.key, this.urlQueryNotifier});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _RequestLineState();\n  }\n}\n\nclass _RequestLineState extends State<_RequestLine> {\n  TextEditingController requestUrl = TextEditingController(text: \"\");\n  HttpMethod requestMethod = HttpMethod.get;\n\n  @override\n  void initState() {\n    super.initState();\n    widget.urlQueryNotifier?.paramListener((param) => onQueryChange(param));\n    if (widget.request == null) {\n      requestUrl.text = 'https://';\n      return;\n    }\n    var request = widget.request!;\n    requestUrl.text = request.requestUrl;\n    requestMethod = request.method;\n  }\n\n  @override\n  dispose() {\n    requestUrl.dispose();\n    super.dispose();\n  }\n\n  void change(String? requestUrl, HttpMethod? requestMethod) {\n    this.requestUrl.text = requestUrl ?? this.requestUrl.text;\n    this.requestMethod = requestMethod ?? this.requestMethod;\n\n    urlNotifier();\n  }\n\n  void urlNotifier() {\n    var splitFirst = requestUrl.text.splitFirst(\"?\".codeUnits.first);\n    widget.urlQueryNotifier?.onUrlChange(splitFirst.length > 1 ? splitFirst.last : '');\n  }\n\n  void onQueryChange(String query) {\n    var url = requestUrl.text;\n    var indexOf = url.indexOf(\"?\");\n    if (indexOf == -1) {\n      requestUrl.text = \"$url?$query\";\n    } else {\n      requestUrl.text = \"${url.substring(0, indexOf)}?$query\";\n    }\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextInput;\n    return TextField(\n        style: const TextStyle(fontSize: 14),\n        minLines: 1,\n        maxLines: 5,\n        autofocus: false,\n        controller: requestUrl,\n        decoration: InputDecoration(\n            prefixIcon: Padding(\n              padding: const EdgeInsets.only(left: 6, right: 6),\n              child: MethodPopupMenu(\n                value: requestMethod,\n                showSeparator: true,\n                onChanged: (val) {\n                  setState(() => requestMethod = val!);\n                },\n              ),\n            ),\n            isDense: true,\n            border: const OutlineInputBorder(borderSide: BorderSide()),\n            enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.grey, width: 0.3))),\n        onChanged: (value) {\n          urlNotifier();\n        });\n  }\n}\n\nclass KeyVal {\n  bool enabled = true;\n  String key;\n  String value;\n\n  KeyVal(this.key, this.value);\n}\n\n///key value\nclass KeyValWidget extends StatefulWidget {\n  final String title;\n  final Map<String, List<String>>? params;\n  final bool readOnly; //只读\n  final UrlQueryNotifier? paramNotifier;\n  final bool expanded;\n  final List<String>? suggestions;\n\n  const KeyValWidget(\n      {super.key,\n      this.params,\n      this.readOnly = false,\n      this.paramNotifier,\n      required this.title,\n      this.expanded = true,\n      this.suggestions});\n\n  @override\n  State<StatefulWidget> createState() {\n    return KeyValState();\n  }\n}\n\nfinal Map<String, bool> _expanded = {};\n\nclass KeyValState extends State<KeyValWidget> {\n  final List<KeyVal> _params = [];\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    widget.params?.forEach((name, values) {\n      for (var val in values) {\n        var keyVal = KeyVal(name, val);\n        _params.add(keyVal);\n      }\n    });\n\n    widget.paramNotifier?.urlListener((url) => onChange(url));\n  }\n\n  //监听url发生变化 更改表单\n  void onChange(String value) {\n    var query = value.split(\"&\");\n    int index = 0;\n    while (index < query.length) {\n      var splitFirst = query[index].splitFirst('='.codeUnits.first);\n      String key = splitFirst.first;\n      String? val = splitFirst.length == 1 ? null : splitFirst.last;\n      if (_params.length <= index) {\n        _params.add(KeyVal(key, val ?? ''));\n        continue;\n      }\n\n      var keyVal = _params[index++];\n      keyVal.key = key;\n      keyVal.value = val ?? '';\n    }\n\n    _params.length = index;\n    setState(() {});\n  }\n\n  void notifierChange() {\n    if (widget.paramNotifier == null) return;\n    String query = _params\n        .where((e) => e.enabled && e.key.isNotEmpty)\n        .map((e) => \"${e.key}=${e.value}\".replaceAll(\"&\", \"%26\"))\n        .join(\"&\");\n    widget.paramNotifier?.onParamChange(query);\n  }\n\n  ///获取所有请求头\n  Map<String, List<String>> getParams() {\n    Map<String, List<String>> map = {};\n    for (var keVal in _params) {\n      if (keVal.key.isEmpty || !keVal.enabled) {\n        continue;\n      }\n      map[keVal.key] ??= [];\n      map[keVal.key]!.add(keVal.value);\n    }\n\n    return map;\n  }\n\n  //刷新param\n  void refreshParam(Map<String, List<String>>? headers) {\n    _params.clear();\n    setState(() {\n      headers?.forEach((name, values) {\n        for (var val in values) {\n          _params.add(KeyVal(name, val));\n        }\n      });\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ExpansionTile(\n      title: Text(widget.title, style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)),\n      tilePadding: const EdgeInsets.only(left: 0, top: 10, bottom: 10),\n      initiallyExpanded: _expanded[widget.title] ?? widget.expanded,\n      onExpansionChanged: (value) => _expanded[widget.title] = value,\n      shape: const Border(),\n      children: [\n        ..._buildRows(),\n        widget.readOnly\n            ? const SizedBox()\n            : Container(\n                alignment: Alignment.center,\n                child: TextButton(\n                    onPressed: () {\n                      var keyVal = KeyVal(\"\", \"\");\n                      _params.add(keyVal);\n                      modifyParam(keyVal);\n                    },\n                    child: Text(localizations.add, textAlign: TextAlign.center))) //添加按钮\n      ],\n    );\n  }\n\n  List<Widget> _buildRows() {\n    List<Widget> list = [];\n\n    for (var element in _params) {\n      Widget headerWidget = Padding(padding: const EdgeInsets.only(top: 5, bottom: 5), child: row(element));\n      if (!widget.readOnly) {\n        headerWidget =\n            InkWell(onTap: () => modifyParam(element), onLongPress: () => deleteHeader(element), child: headerWidget);\n      }\n\n      list.add(headerWidget);\n      list.add(const Divider(thickness: 0.2));\n    }\n\n    return list;\n  }\n\n  //隐藏输入框焦点\n  void hideKeyword(BuildContext context) {\n    FocusScopeNode currentFocus = FocusScope.of(context);\n    if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {\n      currentFocus.focusedChild?.unfocus();\n    }\n  }\n\n  /// 修改请求头\n  void modifyParam(KeyVal keyVal) {\n    //隐藏输入框焦点\n    hideKeyword(context);\n    String headerName = keyVal.key;\n    String val = keyVal.value;\n    showDialog(\n        context: context,\n        builder: (ctx) {\n          return StatefulBuilder(builder: (context, setState) {\n            return AlertDialog(\n              titlePadding: const EdgeInsets.only(left: 25, top: 10),\n              actionsPadding: const EdgeInsets.only(right: 10, bottom: 10),\n              title: Text(localizations.modifyRequestHeader, style: const TextStyle(fontSize: 18)),\n              content: Wrap(\n                children: [\n                  if (widget.suggestions != null)\n                    Autocomplete<String>(\n                      optionsBuilder: (TextEditingValue textEditingValue) {\n                        if (textEditingValue.text.isEmpty) {\n                          return const Iterable<String>.empty();\n                        }\n                        return widget.suggestions!.where((String option) {\n                          return option.toLowerCase().contains(textEditingValue.text.toLowerCase());\n                        });\n                      },\n                      onSelected: (String selection) {\n                        setState(() {\n                          headerName = selection;\n                        });\n                      },\n                      fieldViewBuilder: (BuildContext context, TextEditingController textEditingController,\n                          FocusNode focusNode, VoidCallback onFieldSubmitted) {\n                        return TextFormField(\n                          controller: textEditingController,\n                          focusNode: focusNode,\n                          minLines: 1,\n                          maxLines: 3,\n                          decoration: InputDecoration(labelText: localizations.headerName),\n                          onChanged: (value) {\n                            headerName = value;\n                            setState(() {});\n                          },\n                        );\n                      },\n                      initialValue: TextEditingValue(text: headerName),\n                      optionsViewBuilder:\n                          (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {\n                        return Align(\n                          alignment: Alignment.topLeft,\n                          child: Material(\n                            elevation: 4.0,\n                            child: ConstrainedBox(\n                              constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300),\n                              child: ListView.builder(\n                                padding: EdgeInsets.zero,\n                                shrinkWrap: true,\n                                itemCount: options.length,\n                                itemBuilder: (BuildContext context, int index) {\n                                  final String option = options.elementAt(index);\n                                  return InkWell(\n                                    onTap: () {\n                                      onSelected(option);\n                                    },\n                                    child: Container(\n                                      padding: const EdgeInsets.all(10.0),\n                                      child: _buildHighlightText(option, headerName),\n                                    ),\n                                  );\n                                },\n                              ),\n                            ),\n                          ),\n                        );\n                      },\n                    )\n                  else\n                    TextFormField(\n                      minLines: 1,\n                      maxLines: 3,\n                      initialValue: headerName,\n                      decoration: InputDecoration(labelText: localizations.headerName),\n                      onChanged: (value) {\n                        headerName = value;\n                        setState(() {});\n                      },\n                    ),\n                  if (HttpHeaders.commonHeaderValues.containsKey(headerName))\n                    Autocomplete<String>(\n                      optionsBuilder: (TextEditingValue textEditingValue) {\n                        if (textEditingValue.text.isEmpty) {\n                          return const Iterable<String>.empty();\n                        }\n                        return HttpHeaders.commonHeaderValues[headerName]!.where((String option) {\n                          return option.toLowerCase().contains(textEditingValue.text.toLowerCase());\n                        });\n                      },\n                      onSelected: (String selection) {\n                        val = selection;\n                      },\n                      fieldViewBuilder: (BuildContext context, TextEditingController textEditingController,\n                          FocusNode focusNode, VoidCallback onFieldSubmitted) {\n                        return TextFormField(\n                          controller: textEditingController,\n                          focusNode: focusNode,\n                          minLines: 1,\n                          maxLines: 8,\n                          decoration: InputDecoration(labelText: localizations.value),\n                          onChanged: (value) => val = value,\n                        );\n                      },\n                      initialValue: TextEditingValue(text: val),\n                      optionsViewBuilder:\n                          (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {\n                        return Align(\n                          alignment: Alignment.topLeft,\n                          child: Material(\n                            elevation: 4.0,\n                            child: ConstrainedBox(\n                              constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300),\n                              child: ListView.builder(\n                                padding: EdgeInsets.zero,\n                                shrinkWrap: true,\n                                itemCount: options.length,\n                                itemBuilder: (BuildContext context, int index) {\n                                  final String option = options.elementAt(index);\n                                  return InkWell(\n                                    onTap: () {\n                                      onSelected(option);\n                                    },\n                                    child: Container(\n                                      padding: const EdgeInsets.all(10.0),\n                                      child: _buildHighlightText(option, val),\n                                    ),\n                                  );\n                                },\n                              ),\n                            ),\n                          ),\n                        );\n                      },\n                    )\n                  else\n                    TextFormField(\n                      minLines: 1,\n                      maxLines: 8,\n                      initialValue: val,\n                      decoration: InputDecoration(labelText: localizations.value),\n                      onChanged: (value) => val = value,\n                    )\n                ],\n              ),\n              actions: [\n                TextButton(onPressed: () => Navigator.pop(ctx), child: Text(localizations.cancel)),\n                TextButton(\n                    onPressed: () {\n                      this.setState(() {\n                        keyVal.key = headerName;\n                        keyVal.value = val;\n                      });\n                      notifierChange();\n                      Navigator.pop(ctx);\n                    },\n                    child: Text(localizations.modify)),\n              ],\n            );\n          });\n        });\n  }\n\n  //删除\n  deleteHeader(KeyVal keyVal) {\n    showDialog(\n        context: context,\n        builder: (ctx) {\n          return AlertDialog(\n            title: Text(localizations.deleteHeaderConfirm, style: const TextStyle(fontSize: 18)),\n            actions: [\n              TextButton(onPressed: () => Navigator.pop(ctx), child: Text(localizations.cancel)),\n              TextButton(\n                  onPressed: () {\n                    setState(() => _params.remove(keyVal));\n                    notifierChange();\n                    Navigator.pop(ctx);\n                  },\n                  child: Text(localizations.delete)),\n            ],\n          );\n        });\n  }\n\n  Widget row(KeyVal keyVal) {\n    return Row(children: [\n      if (!widget.readOnly)\n        Checkbox(\n            value: keyVal.enabled,\n            onChanged: (val) {\n              setState(() {\n                keyVal.enabled = val!;\n              });\n              notifierChange();\n            }),\n      Expanded(flex: 4, child: Text(keyVal.key, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))),\n      const Text(\":\", style: TextStyle(color: Colors.orange, fontWeight: FontWeight.w600)),\n      const SizedBox(width: 8),\n      Expanded(\n        flex: 6,\n        child: Text(keyVal.value, style: const TextStyle(fontSize: 13), maxLines: 5, overflow: TextOverflow.ellipsis),\n      ),\n    ]);\n  }\n\n  Widget _buildHighlightText(String text, String query) {\n    if (query.isEmpty) {\n      return Text(text);\n    }\n\n    int index = text.toLowerCase().indexOf(query.toLowerCase());\n    if (index < 0) {\n      return Text(text);\n    }\n\n    return Text.rich(TextSpan(children: [\n      TextSpan(text: text.substring(0, index)),\n      TextSpan(\n          text: text.substring(index, index + query.length),\n          style: TextStyle(color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold)),\n      TextSpan(text: text.substring(index + query.length))\n    ]));\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/request/request_editor_source.dart",
    "content": "enum RequestEditorSource {\n  editor,\n  breakpointRequest,\n  breakpointResponse,\n}\n\n"
  },
  {
    "path": "lib/ui/mobile/request/request_sequence.dart",
    "content": "import 'dart:collection';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/ui/mobile/request/request.dart';\nimport 'package:proxypin/utils/keyword_highlight.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\n\nimport '../../component/model/search_model.dart';\n\n///请求序列 列表\n///@author wanghongen\nclass RequestSequence extends StatefulWidget {\n  final ListenableList<HttpRequest> container;\n  final ProxyServer proxyServer;\n  final bool displayDomain;\n  final bool? sortDesc;\n  final Function(List<HttpRequest>)? onRemove;\n\n  const RequestSequence(\n      {super.key,\n      required this.container,\n      required this.proxyServer,\n      this.displayDomain = true,\n      this.onRemove,\n      this.sortDesc});\n\n  @override\n  State<StatefulWidget> createState() {\n    return RequestSequenceState();\n  }\n}\n\nclass RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliveClientMixin {\n  ///请求id和对应的row的映射\n  Map<String, GlobalKey<RequestRowState>> indexes = HashMap();\n\n  ///显示的请求列表 最新的在前面\n  Queue<HttpRequest> view = Queue();\n  bool changing = false;\n\n  bool sortDesc = true;\n\n  //搜索的内容\n  SearchModel? searchModel;\n\n  //关键词高亮监听\n  late VoidCallback highlightListener;\n\n  @override\n  initState() {\n    super.initState();\n    sortDesc = widget.sortDesc ?? true;\n    view.addAll(widget.container.source.reversed);\n    highlightListener = () {\n      //回调时机在高亮设置页面dispose之后。所以需要在下一帧刷新，否则会报错\n      WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n        setState(() {});\n      });\n    };\n    KeywordHighlights.addListener(highlightListener);\n  }\n\n  @override\n  dispose() {\n    KeywordHighlights.removeListener(highlightListener);\n    super.dispose();\n  }\n\n  ///添加请求\n  add(HttpRequest request) {\n    ///过滤\n    if (searchModel?.isNotEmpty == true && !searchModel!.filter(request, request.response)) {\n      return;\n    }\n\n    if (sortDesc) {\n      view.addFirst(request);\n    } else {\n      view.addLast(request);\n    }\n\n    changeState();\n  }\n\n  ///添加响应\n  addResponse(HttpResponse response) {\n    var state = indexes.remove(response.request?.requestId);\n    state?.currentState?.change(response);\n\n    if (searchModel == null || searchModel!.isEmpty || response.request == null) {\n      return;\n    }\n\n    //搜索视图\n    if (searchModel?.filter(response.request!, response) == true && state == null) {\n      if (!view.contains(response.request)) {\n        view.addFirst(response.request!);\n        changeState();\n      }\n    }\n  }\n\n  clean() {\n    setState(() {\n      view.clear();\n      indexes.clear();\n\n      view.addAll(widget.container.source.reversed);\n    });\n  }\n\n  remove(List<HttpRequest> list) {\n    setState(() {\n      view.removeWhere((element) => list.contains(element));\n    });\n  }\n\n  ///过滤\n  void search(SearchModel searchModel) {\n    this.searchModel = searchModel;\n    if (searchModel.isEmpty) {\n      view = Queue.of(widget.container.source.reversed);\n    } else {\n      view = Queue.of(widget.container.where((it) => searchModel.filter(it, it.response)).toList().reversed);\n    }\n    changeState();\n  }\n\n  Iterable<HttpRequest> currentView() {\n    return view;\n  }\n\n  changeState() {\n    //防止频繁刷新\n    if (!changing) {\n      changing = true;\n      Future.delayed(const Duration(milliseconds: 350), () {\n        setState(() {\n          changing = false;\n        });\n      });\n    }\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    return Scrollbar(\n        controller: PrimaryScrollController.maybeOf(context),\n        child: ListView.separated(\n            controller: PrimaryScrollController.maybeOf(context),\n            cacheExtent: 1000,\n            separatorBuilder: (context, index) =>\n                Divider(thickness: 0.2, height: 0, color: Theme.of(context).dividerColor),\n            itemCount: view.length,\n            itemBuilder: (context, index) {\n              final requestId = view.elementAt(index).requestId;\n\n              final key = GlobalKey<RequestRowState>();\n              indexes[requestId] = key;\n\n              return RequestRow(\n                  index: sortDesc ? view.length - index : index,\n                  key: key,\n                  request: view.elementAt(index),\n                  proxyServer: widget.proxyServer,\n                  displayDomain: widget.displayDomain,\n                  onRemove: (request) {\n                    setState(() {\n                      view.remove(request);\n                      indexes.remove(requestId);\n                    });\n                    widget.onRemove?.call([request]);\n                  });\n            }));\n  }\n\n  scrollToTop() {\n    PrimaryScrollController.maybeOf(context)\n        ?.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.ease);\n  }\n\n  ///排序\n  sort(bool desc) {\n    if (sortDesc == desc) {\n      return;\n    }\n\n    sortDesc = desc;\n    setState(() {\n      view = Queue.of(view.toList().reversed);\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/request/search.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/ui/component/search_condition.dart';\n\nimport '../../component/model/search_model.dart';\n\nclass MobileSearch extends StatefulWidget {\n  final Function(SearchModel searchModel)? onSearch;\n  final bool showSearch;\n\n  const MobileSearch({super.key, this.onSearch, this.showSearch = false});\n\n  @override\n  State<StatefulWidget> createState() {\n    return MobileSearchState();\n  }\n}\n\nclass MobileSearchState extends State<MobileSearch> {\n  SearchModel searchModel = SearchModel();\n  bool _searched = false;\n  final TextEditingController _keywordController = TextEditingController();\n  bool _changing = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (widget.showSearch) {\n        showSearch();\n      }\n    });\n  }\n\n  @override\n  dispose() {\n    _keywordController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n        padding: const EdgeInsets.only(left: 0),\n        child: TextFormField(\n            controller: _keywordController,\n            textAlignVertical: TextAlignVertical.center,\n            cursorHeight: 20,\n            keyboardType: TextInputType.url,\n            onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n            onChanged: (val) {\n              searchModel.keyword = val;\n              if (!_changing) {\n                _changing = true;\n                Future.delayed(const Duration(milliseconds: 500), () {\n                  _changing = false;\n                  if (!_searched) {\n                    searchModel.searchOptions = {Option.url, Option.method, Option.responseContentType};\n                  }\n                  widget.onSearch?.call(searchModel);\n                });\n              }\n            },\n            decoration: InputDecoration(\n                border: InputBorder.none,\n                prefixIcon: InkWell(\n                    onTap: showSearch,\n                    child: Icon(Icons.search, color: _searched ? Colors.green : Theme.of(context).colorScheme.primary)),\n                hintText: 'Search')));\n  }\n\n  void showSearch() {\n    showModalBottomSheet(\n        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),\n        isScrollControlled: true,\n        context: context,\n        builder: (context) {\n          if (!_searched) {\n            searchModel.searchOptions = {Option.url};\n          }\n          return Padding(\n              padding: MediaQuery.of(context).viewInsets,\n              child: Container(\n                  constraints: BoxConstraints(minHeight: 450,maxHeight:  480),\n                  child: SearchConditions(\n                    padding: const EdgeInsets.only(left: 15, right: 15, top: 10),\n                    searchModel: searchModel,\n                    onSearch: (val) {\n                      setState(() {\n                        searchModel = val;\n                        _searched = searchModel.isNotEmpty;\n                        _keywordController.text = searchModel.keyword ?? '';\n                        widget.onSearch?.call(searchModel);\n                      });\n                    },\n                  )));\n        });\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/app_filter.dart",
    "content": "﻿/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:typed_data';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/native/installed_apps.dart';\nimport 'package:proxypin/native/vpn.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/utils/task.dart';\n\n///应用白名单 目前只支持安卓 ios没办法获取安装的列表\n///@author wang\nclass AppWhitelist extends StatefulWidget {\n  final ProxyServer proxyServer;\n\n  const AppWhitelist({super.key, required this.proxyServer});\n\n  @override\n  State<AppWhitelist> createState() => _AppWhitelistState();\n}\n\nclass _AppWhitelistState extends State<AppWhitelist> {\n  late Configuration configuration;\n\n  bool changed = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    configuration = widget.proxyServer.configuration;\n  }\n\n  @override\n  void dispose() {\n    if (changed) {\n      configuration.flushConfig();\n      if (Vpn.isVpnStarted) {\n        Vpn.restartVpn(\"127.0.0.1\", widget.proxyServer.port, configuration);\n      }\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    var appWhitelist = <Future<AppInfo>>[];\n    for (var element in configuration.appWhitelist) {\n      appWhitelist.add(InstalledApps.getAppInfo(element).catchError((e) {\n        return AppInfo(name: isCN ? \"未知应用\" : \"Unknown app\", packageName: element, inValid: true);\n      }));\n    }\n\n    return Scaffold(\n        appBar: AppBar(\n          title: Text(localizations.appWhitelist, style: const TextStyle(fontSize: 16)),\n          actions: [\n            IconButton(\n              icon: const Icon(Icons.add),\n              onPressed: () async {\n                //添加\n                List<AppInfo> list = await Future.wait(appWhitelist);\n                if (context.mounted) {\n                  Navigator.of(context).push(MaterialPageRoute(builder: (context) {\n                    return InstalledAppsWidget(addedList: list);\n                  })).then((value) {\n                    if (value != null) {\n                      if (configuration.appWhitelist.contains(value)) {\n                        return;\n                      }\n                      setState(() {\n                        configuration.appWhitelist.add(value);\n                        changed = true;\n                      });\n                    }\n                  });\n                }\n              },\n            ),\n            IconButton(\n              tooltip: isCN ? '清除失效应用' : 'clear invalid apps',\n              onPressed: () async {\n                if (configuration.appWhitelist.isEmpty) return;\n                List<AppInfo> list = await Future.wait(appWhitelist);\n                for (AppInfo appInfo in list) {\n                  if (appInfo.inValid == true) {\n                    configuration.appWhitelist.remove(appInfo.packageName);\n                  }\n                }\n                setState(() {\n                  changed = true;\n                });\n              },\n              icon: Icon(Icons.cleaning_services_outlined),\n            ),\n          ],\n        ),\n        body: Column(children: [\n          const SizedBox(height: 5),\n          SwitchWidget(\n              value: configuration.appWhitelistEnabled,\n              title: localizations.enable,\n              subtitle: localizations.appWhitelistDescribe,\n              onChanged: (val) {\n                changed = true;\n                configuration.appWhitelistEnabled = val;\n                configuration.flushConfig();\n              }),\n          const SizedBox(height: 5),\n          Expanded(\n              child: FutureBuilder(\n                  future: Future.wait(appWhitelist),\n                  builder: (BuildContext context, AsyncSnapshot<List<AppInfo>> snapshot) {\n                    if (snapshot.hasData) {\n                      if (snapshot.data!.isEmpty) {\n                        return Center(\n                          child: Padding(\n                              padding: const EdgeInsets.symmetric(horizontal: 15),\n                              child: Text(\n                                  isCN\n                                      ? \"未设置白名单应用时会对所有应用抓包\"\n                                      : \"When no whitelist application is set, all applications will be captured\",\n                                  style: const TextStyle(color: Colors.grey))),\n                        );\n                      }\n\n                      return ListView.builder(\n                          itemCount: snapshot.data!.length,\n                          itemBuilder: (BuildContext context, int index) {\n                            AppInfo appInfo = snapshot.data![index];\n                            return ListTile(\n                              leading:\n                                  appInfo.icon == null ? const Icon(Icons.question_mark) : Image.memory(appInfo.icon!),\n                              title: Text(appInfo.name ?? \"\"),\n                              subtitle: Text(appInfo.packageName ?? \"\"),\n                              trailing: IconButton(\n                                icon: const Icon(Icons.delete),\n                                onPressed: () {\n                                  //删除\n                                  setState(() {\n                                    configuration.appWhitelist.remove(appInfo.packageName);\n                                    changed = true;\n                                  });\n                                },\n                              ),\n                            );\n                          });\n                    } else {\n                      return const Center(\n                        child: CircularProgressIndicator(),\n                      );\n                    }\n                  })),\n        ]));\n  }\n}\n\nclass AppBlacklist extends StatefulWidget {\n  final ProxyServer proxyServer;\n\n  const AppBlacklist({super.key, required this.proxyServer});\n\n  @override\n  State<AppBlacklist> createState() => _AppBlacklistState();\n}\n\nclass _AppBlacklistState extends State<AppBlacklist> {\n  late Configuration configuration;\n\n  bool changed = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    configuration = widget.proxyServer.configuration;\n  }\n\n  @override\n  void dispose() {\n    if (changed) {\n      configuration.flushConfig();\n      if (Vpn.isVpnStarted) {\n        Vpn.restartVpn(\"127.0.0.1\", widget.proxyServer.port, configuration);\n      }\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n    var appBlacklist = <Future<AppInfo>>[];\n    for (var element in configuration.appBlacklist ?? []) {\n      appBlacklist.add(InstalledApps.getAppInfo(element).catchError((e) {\n        return AppInfo(name: isCN ? \"未知应用\" : \"Unknown app\", packageName: element, inValid: true);\n      }));\n    }\n\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(localizations.appBlacklist, style: const TextStyle(fontSize: 16)),\n        actions: [\n          IconButton(\n            icon: const Icon(Icons.add),\n            onPressed: () async {\n              //添加\n              List<AppInfo> list = await Future.wait(appBlacklist);\n              if (context.mounted) {\n                Navigator.of(context)\n                    .push(MaterialPageRoute(builder: (context) => InstalledAppsWidget(addedList: list)))\n                    .then((value) {\n                  if (value != null) {\n                    if (configuration.appBlacklist?.contains(value) == true) {\n                      return;\n                    }\n                    setState(() {\n                      configuration.appBlacklist ??= [];\n                      configuration.appBlacklist?.add(value);\n                      changed = true;\n                    });\n                  }\n                });\n              }\n            },\n          ),\n          IconButton(\n            tooltip: isCN ? '清除失效应用' : 'clear invalid apps',\n            onPressed: () async {\n              if (configuration.appBlacklist?.isEmpty == true) return;\n              List<AppInfo> list = await Future.wait(appBlacklist);\n              for (AppInfo appInfo in list) {\n                if (appInfo.inValid == true) {\n                  configuration.appBlacklist?.remove(appInfo.packageName);\n                }\n              }\n              setState(() {\n                changed = true;\n              });\n            },\n            icon: Icon(Icons.cleaning_services_outlined),\n          ),\n        ],\n      ),\n      body: FutureBuilder(\n          future: Future.wait(appBlacklist),\n          builder: (BuildContext context, AsyncSnapshot<List<AppInfo>> snapshot) {\n            if (snapshot.hasData) {\n              if (snapshot.data!.isEmpty) {\n                return Center(\n                  child: Padding(\n                      padding: const EdgeInsets.symmetric(horizontal: 15),\n                      child: Text(localizations.emptyData, style: const TextStyle(color: Colors.grey))),\n                );\n              }\n\n              return ListView.builder(\n                  itemCount: snapshot.data!.length,\n                  itemBuilder: (BuildContext context, int index) {\n                    AppInfo appInfo = snapshot.data![index];\n                    return ListTile(\n                      leading: appInfo.icon == null ? const Icon(Icons.question_mark) : Image.memory(appInfo.icon!),\n                      title: Text(appInfo.name ?? \"\"),\n                      subtitle: Text(appInfo.packageName ?? \"\"),\n                      trailing: IconButton(\n                        icon: const Icon(Icons.delete),\n                        onPressed: () {\n                          //删除\n                          setState(() {\n                            configuration.appBlacklist?.remove(appInfo.packageName);\n                            changed = true;\n                          });\n                        },\n                      ),\n                    );\n                  });\n            } else {\n              return const Center(\n                child: CircularProgressIndicator(),\n              );\n            }\n          }),\n    );\n  }\n}\n\n///已安装的app列表\nclass InstalledAppsWidget extends StatefulWidget {\n  const InstalledAppsWidget({\n    super.key,\n    required this.addedList,\n  });\n\n  final List<AppInfo> addedList;\n\n  @override\n  State<InstalledAppsWidget> createState() => _InstalledAppsWidgetState();\n}\n\nclass _InstalledAppsWidgetState extends State<InstalledAppsWidget> {\n  static List<AppInfo>? apps;\n  static bool includeSystemApps = false;\n\n  RxBool loading = false.obs;\n\n  String? keyword;\n\n  @override\n  void initState() {\n    super.initState();\n    DelayedTask().cancel(\"InstalledAppsWidget_release\");\n    if (apps != null) {\n      return;\n    }\n    refreshApps();\n  }\n\n  @override\n  void dispose() {\n    DelayedTask().debounce(\"InstalledAppsWidget_release\", const Duration(seconds: 10), () {\n      apps = null;\n      includeSystemApps = false;\n    });\n    super.dispose();\n  }\n\n  void refreshApps() async {\n    try {\n      loading.value = true;\n      apps = await InstalledApps.getInstalledApps(true, includeSystemApps: includeSystemApps);\n    } finally {\n      loading.value = false;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return Scaffold(\n      appBar: AppBar(\n        title: TextField(\n          decoration: InputDecoration(\n            hintText: isCN ? \"请输入应用名或包名\" : \"Please enter the application or package name\",\n            border: InputBorder.none,\n            hintStyle: TextStyle(color: Colors.grey.shade500),\n            suffixIcon: IconButton(\n              color: includeSystemApps ? Theme.of(context).colorScheme.primary : null,\n              icon: const Icon(Icons.visibility_outlined),\n              tooltip: isCN ? \"显示系统应用\" : \"Show system apps\",\n              onPressed: () {\n                setState(() {\n                  includeSystemApps = !includeSystemApps;\n                });\n                refreshApps();\n              },\n            ),\n          ),\n          onChanged: (String value) {\n            keyword = value.toLowerCase();\n            setState(() {});\n          },\n        ),\n      ),\n      body: RefreshIndicator(\n        onRefresh: () async {\n          refreshApps();\n        },\n        child: Obx(() => loading.value\n            ? const Center(\n                child: CircularProgressIndicator(),\n              )\n            : buildAppListView()),\n      ),\n    );\n  }\n\n  ListView buildAppListView() {\n    if (apps == null) {\n      return ListView();\n    }\n    List<AppInfo> appInfoList = apps!;\n    appInfoList = appInfoList.toSet().difference(widget.addedList.toSet()).toList();\n    if (keyword != null && keyword!.trim().isNotEmpty) {\n      appInfoList = appInfoList\n          .where((element) =>\n              element.name!.toLowerCase().contains(keyword!) || element.packageName!.toLowerCase().contains(keyword!))\n          .toList();\n    }\n\n    return ListView.builder(\n        itemCount: appInfoList.length,\n        itemBuilder: (BuildContext context, int index) {\n          AppInfo appInfo = appInfoList[index];\n          return ListTile(\n            leading: Image.memory(appInfo.icon ?? Uint8List(0)),\n            title: Text(appInfo.name ?? \"\"),\n            subtitle: Text(appInfo.packageName ?? \"\"),\n            onTap: () async {\n              Navigator.of(context).pop(appInfo.packageName);\n            },\n          );\n        });\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/filter.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:collection';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:share_plus/share_plus.dart';\n\nimport '../../../network/components/host_filter.dart';\n\nclass MobileFilterWidget extends StatefulWidget {\n  final Configuration configuration;\n  final HostList hostList;\n\n  const MobileFilterWidget({super.key, required this.configuration, required this.hostList});\n\n  @override\n  State<MobileFilterWidget> createState() => _MobileFilterState();\n}\n\nclass _MobileFilterState extends State<MobileFilterWidget> {\n  final ValueNotifier<bool> hostEnableNotifier = ValueNotifier(false);\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void dispose() {\n    hostEnableNotifier.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var title = widget.hostList.runtimeType == Whites ? localizations.domainWhitelist : localizations.domainBlacklist;\n    var subtitle =\n        widget.hostList.runtimeType == Whites ? localizations.domainWhitelistDescribe : localizations.domainBlacklist;\n\n    return Scaffold(\n        appBar: AppBar(title: Text(localizations.domainFilter, style: const TextStyle(fontSize: 16))),\n        body: Container(\n          padding: const EdgeInsets.all(10),\n          child: DomainFilter(\n              title: title,\n              subtitle: subtitle,\n              hostList: widget.hostList,\n              configuration: widget.configuration,\n              hostEnableNotifier: hostEnableNotifier),\n        ));\n  }\n}\n\nclass DomainFilter extends StatefulWidget {\n  final String title;\n  final String subtitle;\n  final HostList hostList;\n  final Configuration configuration;\n  final ValueNotifier<bool> hostEnableNotifier;\n\n  const DomainFilter(\n      {super.key,\n      required this.title,\n      required this.subtitle,\n      required this.hostList,\n      required this.hostEnableNotifier,\n      required this.configuration});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _DomainFilterState();\n  }\n}\n\nclass _DomainFilterState extends State<DomainFilter> {\n  bool changed = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void dispose() {\n    if (changed) {\n      widget.configuration.flushConfig();\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        ListTile(title: Text(widget.title), subtitle: Text(widget.subtitle, style: const TextStyle(fontSize: 12))),\n        ValueListenableBuilder(\n            valueListenable: widget.hostEnableNotifier,\n            builder: (_, bool enable, __) {\n              return SwitchListTile(\n                  title: Text(localizations.enable),\n                  value: widget.hostList.enabled,\n                  onChanged: (value) {\n                    widget.hostList.enabled = value;\n                    changed = true;\n                    widget.hostEnableNotifier.value = !widget.hostEnableNotifier.value;\n                  });\n            }),\n        Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n          TextButton.icon(icon: const Icon(Icons.add, size: 20), onPressed: add, label: Text(localizations.add)),\n          const SizedBox(width: 10),\n          TextButton.icon(\n              icon: const Icon(Icons.input_rounded, size: 20), onPressed: import, label: Text(localizations.import)),\n          const SizedBox(width: 5),\n        ]),\n        Expanded(child: DomainList(widget.hostList, onChange: () => changed = true))\n      ],\n    );\n  }\n\n  //导入\n  import() async {\n    final FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any);\n    if (result == null || result.files.isEmpty) {\n      return;\n    }\n    var file = File(result.files.single.path!);\n    try {\n      List json = jsonDecode(await file.readAsString());\n      for (var item in json) {\n        widget.hostList.add(item);\n      }\n\n      changed = true;\n      if (mounted) {\n        FlutterToastr.show(localizations.importSuccess, context);\n      }\n      setState(() {});\n    } catch (e, t) {\n      logger.e('导入失败 $file', error: e, stackTrace: t);\n      if (mounted) {\n        FlutterToastr.show(\"${localizations.importFailed} $e\", context);\n      }\n    }\n  }\n\n  void add() {\n    showDialog(context: context, builder: (BuildContext context) => DomainAddDialog(hostList: widget.hostList))\n        .then((value) {\n      if (value != null) {\n        setState(() {\n          changed = true;\n        });\n      }\n    });\n  }\n}\n\nclass DomainAddDialog extends StatelessWidget {\n  final HostList hostList;\n  final int? index;\n\n  const DomainAddDialog({super.key, required this.hostList, this.index});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    GlobalKey formKey = GlobalKey<FormState>();\n    String? host = index == null ? null : hostList.list.elementAt(index!).pattern.replaceAll(\".*\", \"*\");\n    return AlertDialog(\n        scrollable: true,\n        content: Padding(\n            padding: const EdgeInsets.all(8.0),\n            child: Form(\n                key: formKey,\n                child: Column(children: <Widget>[\n                  TextFormField(\n                      initialValue: host,\n                      decoration: const InputDecoration(labelText: 'Host', hintText: '*.example.com'),\n                      validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,\n                      onChanged: (val) => host = val)\n                ]))),\n        actions: [\n          TextButton(child: Text(localizations.cancel), onPressed: () => Navigator.of(context).pop()),\n          TextButton(\n              child: Text(localizations.save),\n              onPressed: () {\n                if (!(formKey.currentState as FormState).validate()) {\n                  return;\n                }\n                try {\n                  if (index != null) {\n                    hostList.list[index!] = RegExp(host!.trim().replaceAll(\"*\", \".*\"));\n                  } else {\n                    hostList.add(host!.trim());\n                  }\n                } catch (e) {\n                  ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));\n                }\n                Navigator.of(context).pop(host);\n              }),\n        ]);\n  }\n}\n\n///域名列表\nclass DomainList extends StatefulWidget {\n  final HostList hostList;\n  final Function onChange;\n\n  const DomainList(this.hostList, {super.key, required this.onChange});\n\n  @override\n  State<StatefulWidget> createState() => _DomainListState();\n}\n\nclass _DomainListState extends State<DomainList> {\n  Set<int> selected = HashSet<int>();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n  bool changed = false;\n  bool multiple = false;\n\n  onChanged() {\n    changed = true;\n    widget.onChange.call();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        persistentFooterButtons: multiple ? [globalMenu()] : null,\n        body: Container(\n            padding: const EdgeInsets.only(top: 10),\n            decoration: BoxDecoration(\n              border: Border.all(color: Colors.grey.withOpacity(0.2)),\n            ),\n            child: Scrollbar(\n                child: ListView(children: [\n              Row(\n                mainAxisAlignment: MainAxisAlignment.start,\n                children: [\n                  Container(width: 15),\n                  const Expanded(child: Text('Host')),\n                ],\n              ),\n              const Divider(thickness: 0.5),\n              Column(children: rows(widget.hostList.list))\n            ]))));\n  }\n\n  globalMenu() {\n    return Stack(children: [\n      Container(\n          height: 50,\n          width: double.infinity,\n          margin: const EdgeInsets.only(top: 10),\n          decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2)))),\n      Positioned(\n          top: 0,\n          left: 0,\n          right: 0,\n          child: Center(\n              child: TextButton(\n                  onPressed: () {},\n                  child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [\n                    TextButton.icon(\n                        onPressed: () {\n                          export(selected.toList());\n                          setState(() {\n                            selected.clear();\n                            multiple = false;\n                          });\n                        },\n                        icon: const Icon(Icons.share, size: 18),\n                        label: Text(localizations.export, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () => remove(),\n                        icon: const Icon(Icons.delete, size: 18),\n                        label: Text(localizations.delete, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () {\n                          setState(() {\n                            multiple = false;\n                            selected.clear();\n                          });\n                        },\n                        icon: const Icon(Icons.cancel, size: 18),\n                        label: Text(localizations.cancel, style: const TextStyle(fontSize: 14))),\n                  ]))))\n    ]);\n  }\n\n  List<Widget> rows(List<RegExp> list) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n\n    return List.generate(list.length, (index) {\n      return InkWell(\n          enableFeedback: false,\n          highlightColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          hoverColor: primaryColor.withOpacity(0.3),\n          onLongPress: () => showMenus(index),\n          // menus\n          onDoubleTap: () => showEdit(index),\n          onTap: () {\n            if (multiple) {\n              setState(() {\n                if (!selected.add(index)) {\n                  selected.remove(index);\n                }\n              });\n              return;\n            }\n            showEdit(index);\n          },\n          child: Container(\n              color: selected.contains(index)\n                  ? primaryColor.withOpacity(0.8)\n                  : index.isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 38,\n              padding: const EdgeInsets.symmetric(vertical: 3),\n              child: Row(\n                children: [\n                  const SizedBox(width: 15),\n                  Expanded(\n                      child: Text(list[index].pattern.replaceAll(\".*\", \"*\"), style: const TextStyle(fontSize: 14))),\n                ],\n              )));\n    });\n  }\n\n  showEdit([int? index]) {\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (BuildContext context) {\n          return DomainAddDialog(hostList: widget.hostList, index: index);\n        }).then((value) {\n      if (value != null) {\n        setState(() {\n          onChanged();\n        });\n      }\n    });\n  }\n\n  //点击菜单\n  showMenus(int index) {\n    setState(() {\n      selected.add(index);\n    });\n    HapticFeedback.mediumImpact();\n\n    showCupertinoModalPopup(\n        context: context,\n        builder: (BuildContext context) {\n          return CupertinoActionSheet(\n              actions: [\n                CupertinoActionSheetAction(\n                    child: Text(localizations.multiple),\n                    onPressed: () {\n                      setState(() => multiple = true);\n                      Navigator.of(context).pop();\n                    }),\n                CupertinoActionSheetAction(\n                    onPressed: () {\n                      Clipboard.setData(ClipboardData(text: widget.hostList.list[index].pattern.replaceAll(\".*\", \"*\")));\n                      FlutterToastr.show(localizations.copied, context);\n                      Navigator.of(context).pop();\n                    },\n                    child: Text(localizations.copy)),\n                CupertinoActionSheetAction(\n                    onPressed: () {\n                      Navigator.of(context).pop();\n                      showEdit(index);\n                    },\n                    child: Text(localizations.edit)),\n                CupertinoActionSheetAction(\n                    onPressed: () {\n                      export([index]);\n                      Navigator.of(context).pop();\n                    },\n                    child: Text(localizations.share)),\n                CupertinoActionSheetAction(\n                    onPressed: () {\n                      widget.hostList.removeIndex([index]);\n                      onChanged();\n                      Navigator.of(context).pop();\n                    },\n                    child: Text(localizations.delete)),\n              ],\n              cancelButton: CupertinoActionSheetAction(\n                  onPressed: () {\n                    Navigator.of(context).pop();\n                  },\n                  child: Text(localizations.cancel)));\n        }).then((value) {\n      if (multiple) {\n        return;\n      }\n      setState(() {\n        selected.remove(index);\n      });\n    });\n  }\n\n  //导出\n  export(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n\n    String fileName = 'host-filters.config';\n    var list = [];\n    for (var index in indexes) {\n      String rule = widget.hostList.list[index].pattern;\n      list.add(rule);\n    }\n\n    RenderBox? box;\n    if (await Platforms.isIpad() && mounted) {\n      box = context.findRenderObject() as RenderBox?;\n    }\n\n    final XFile file = XFile.fromData(utf8.encode(jsonEncode(list)), mimeType: 'config');\n    await Share.shareXFiles([file],\n        fileNameOverrides: [fileName],\n        sharePositionOrigin: box == null ? null : box.localToGlobal(Offset.zero) & box.size);\n  }\n\n  //删除\n  Future<void> remove() async {\n    if (selected.isEmpty) return;\n\n    return showConfirmDialog(context, content: localizations.requestRewriteDeleteConfirm(selected.length),\n        onConfirm: () async {\n      widget.hostList.removeIndex(selected.toList());\n      onChanged();\n      setState(() {\n        multiple = false;\n        selected.clear();\n      });\n      if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/hosts.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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\nimport 'dart:convert';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/components/manager/hosts_manager.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\n/// Hosts page\n/// @author wanghongen\nclass HostsPage extends StatefulWidget {\n  final HostsManager hostsManager;\n\n  const HostsPage({super.key, required this.hostsManager});\n\n  @override\n  State<StatefulWidget> createState() => _HostsPageState();\n}\n\nclass _HostsPageState extends State<HostsPage> {\n  late HostsManager hostsManager = widget.hostsManager;\n  Set<HostsItem> selected = {};\n  Set<String> offstage = {};\n\n  bool multiple = false;\n\n  bool saving = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  saveConfig() {\n    if (saving) return;\n    saving = true;\n    Future.delayed(const Duration(milliseconds: 3000), () {\n      widget.hostsManager.flushConfig();\n      saving = false;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(centerTitle: true, title: Text('Hosts', style: const TextStyle(fontSize: 16))),\n        persistentFooterButtons: [multiple ? globalMenu() : const SizedBox()],\n        body: Padding(\n            padding: const EdgeInsets.all(8),\n            child: Column(\n              children: <Widget>[\n                Row(children: [\n                  Container(width: 15),\n                  Text(localizations.enable),\n                  const SizedBox(width: 10),\n                  SwitchWidget(\n                      scale: 0.8,\n                      value: widget.hostsManager.enabled,\n                      onChanged: (value) {\n                        widget.hostsManager.enabled = value;\n                        saveConfig();\n                      }),\n                ]),\n                Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n                  TextButton.icon(\n                      icon: const Icon(Icons.add, size: 18), onPressed: showEdit, label: Text(localizations.newBuilt)),\n                  TextButton.icon(\n                      icon: const Icon(Icons.folder_outlined, size: 18),\n                      onPressed: newFolder,\n                      label: Text(localizations.newFolder)),\n                  TextButton.icon(\n                      icon: const Icon(Icons.input_rounded, size: 18),\n                      onPressed: import,\n                      label: Text(localizations.import)),\n                  SizedBox(width: 3),\n                ]),\n                const SizedBox(height: 8),\n                Expanded(\n                    child: Column(children: [\n                  const SizedBox(height: 5),\n                  Row(children: [\n                    Container(width: 15),\n                    SizedBox(width: 50, child: Text(localizations.enable, style: const TextStyle(fontSize: 14))),\n                    Container(width: 15),\n                    Expanded(child: Text(localizations.domain, style: TextStyle(fontSize: 14))),\n                    Container(width: 15),\n                    Expanded(child: Text(localizations.toAddress, style: const TextStyle(fontSize: 14))),\n                  ]),\n                  const Divider(thickness: 0.5),\n                  Expanded(\n                      child: ListView.builder(\n                          shrinkWrap: true,\n                          itemCount: widget.hostsManager.list.length,\n                          padding: const EdgeInsets.only(right: 10),\n                          itemBuilder: (_, index) => row(widget.hostsManager.list[index], index.isEven)))\n                ])),\n              ],\n            )));\n  }\n\n  Widget row(HostsItem item, bool isEven, {EdgeInsetsGeometry? padding}) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n\n    return Column(children: [\n      GestureDetector(\n          onLongPressStart: (details) => showMenus(details, item),\n          onTap: () {\n            if (multiple) {\n              setState(() {\n                selected.contains(item) ? selected.remove(item) : selected.add(item);\n              });\n              return;\n            }\n\n            if (item.isFolder) {\n              setState(() {\n                offstage.contains(item.id) ? offstage.remove(item.id) : offstage.add(item.id);\n              });\n              return;\n            }\n            showEdit(item: item);\n          },\n          child: Container(\n              color: selected.contains(item)\n                  ? primaryColor.withOpacity(0.6)\n                  : isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 42,\n              padding: padding ?? const EdgeInsets.symmetric(vertical: 3),\n              child: Row(\n                children: [\n                  SwitchWidget(\n                      scale: 0.6,\n                      value: item.enabled,\n                      onChanged: (val) {\n                        setState(() {\n                          item.enabled = val;\n                          saveConfig();\n                        });\n                      }),\n                  Container(width: 15),\n                  Expanded(\n                      child: IconText(\n                          icon: item.isFolder\n                              ? Icon(offstage.contains(item.id) ? Icons.folder : Icons.folder_outlined, size: 18)\n                              : null,\n                          text: item.host,\n                          textStyle: const TextStyle(fontSize: 14))),\n                  Container(width: 15),\n                  Expanded(child: Text(item.toAddress ?? '', style: const TextStyle(fontSize: 14)))\n                ],\n              ))),\n      if (item.isFolder)\n        Offstage(\n            offstage: offstage.contains(item.id),\n            child: Column(\n                children: widget.hostsManager\n                    .getFolderList(item.id)\n                    .map((e) => row(e, !isEven, padding: EdgeInsets.only(left: 60)))\n                    .toList()))\n    ]);\n  }\n\n  newFolder() {\n    showEdit(isFolder: true);\n  }\n\n  showEdit({HostsItem? item, HostsItem? parent, bool? isFolder}) {\n    isFolder ??= item?.isFolder == true;\n    showDialog(\n        context: context,\n        builder: (BuildContext context) => isFolder == true\n            ? FolderDialog(hostsManager: widget.hostsManager, folder: item)\n            : HostsEditDialog(item: item, parent: parent)).then((value) {\n      if (value != null) {\n        setState(() {\n          saveConfig();\n        });\n      }\n    });\n  }\n\n  globalMenu() {\n    return Stack(children: [\n      Container(\n          height: 50,\n          width: double.infinity,\n          margin: const EdgeInsets.only(top: 10),\n          decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2)))),\n      Positioned(\n          top: 0,\n          left: 0,\n          right: 0,\n          child: Center(\n              child: TextButton(\n                  onPressed: () {},\n                  child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [\n                    TextButton.icon(\n                        onPressed: () {\n                          export(selected);\n                          setState(() {\n                            selected.clear();\n                            multiple = false;\n                          });\n                        },\n                        icon: const Icon(Icons.share, size: 18),\n                        label: Text(localizations.export, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () => removeHosts(selected),\n                        icon: const Icon(Icons.delete, size: 18),\n                        label: Text(localizations.delete, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () {\n                          setState(() {\n                            multiple = false;\n                            selected.clear();\n                          });\n                        },\n                        icon: const Icon(Icons.cancel, size: 18),\n                        label: Text(localizations.cancel, style: const TextStyle(fontSize: 14))),\n                  ]))))\n    ]);\n  }\n\n  //点击菜单\n  showMenus(LongPressStartDetails details, HostsItem item) {\n    //长按反馈\n    HapticFeedback.lightImpact();\n\n    setState(() {\n      selected.add(item);\n    });\n\n    showContextMenu(context, details.globalPosition, items: [\n      if (item.isFolder)\n        PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit(parent: item)),\n      PopupMenuItem(height: 35, child: Text(localizations.multiple), onTap: () => setState(() => multiple = true)),\n      PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(item: item)),\n      PopupMenuItem(height: 35, onTap: () => export([item]), child: Text(localizations.export)),\n      PopupMenuItem(\n          height: 35,\n          child: item.enabled ? Text(localizations.disabled) : Text(localizations.enable),\n          onTap: () {\n            setState(() {\n              item.enabled = !item.enabled;\n              saveConfig();\n            });\n          }),\n      const PopupMenuDivider(),\n      PopupMenuItem(\n          height: 35,\n          child: Text(localizations.delete),\n          onTap: () async {\n            setState(() {\n              widget.hostsManager.removeHosts([item]);\n            });\n          })\n    ]).then((value) {\n      setState(() {\n        selected.remove(item);\n      });\n    });\n  }\n\n  //删除\n  Future<void> removeHosts(Set<HostsItem> items) async {\n    if (items.isEmpty) return;\n    return showConfirmDialog(context, onConfirm: () async {\n      await widget.hostsManager.removeHosts(items);\n      setState(() {\n        multiple = false;\n        items.clear();\n      });\n      if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n    });\n  }\n\n  //导入\n  import() async {\n    final FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any);\n    var file = result?.files.single;\n    if (file == null) {\n      return;\n    }\n\n    try {\n      List json = jsonDecode(await file.xFile.readAsString());\n      Map<String, String> idMap = {};\n\n      for (var item in json) {\n        //生成新的id 保存映射关系\n        String newId = HostsItem.generateId();\n        idMap[item['id']] = newId;\n        item['id'] = newId;\n        var hostsItem = HostsItem.fromJson(item);\n\n        if (hostsItem.parent != null) {\n          hostsItem.parent = idMap[hostsItem.parent!];\n        }\n\n        widget.hostsManager.addHosts(hostsItem);\n      }\n\n      saveConfig();\n      if (mounted) {\n        FlutterToastr.show(localizations.importSuccess, context);\n      }\n      setState(() {});\n    } catch (e, t) {\n      logger.e('导入失败 $file', error: e, stackTrace: t);\n      if (mounted) {\n        FlutterToastr.show(\"${localizations.importFailed} $e\", context);\n      }\n    }\n  }\n\n  //导出\n  export(Iterable<HostsItem> items) async {\n    if (items.isEmpty) return;\n\n    String fileName = 'hosts.json';\n    var list = [];\n    for (var item in items) {\n      var json = item.toJson();\n      list.add(json);\n    }\n\n    var path = await FilePicker.platform.saveFile(fileName: fileName, bytes: utf8.encode(jsonEncode(list)));\n    if (path == null) {\n      return;\n    }\n    if (mounted) FlutterToastr.show(localizations.exportSuccess, context);\n  }\n}\n\nclass FolderDialog extends StatelessWidget {\n  final HostsManager hostsManager;\n  final HostsItem? folder;\n\n  const FolderDialog({super.key, required this.hostsManager, this.folder});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    bool enabled = folder?.enabled ?? true;\n    String name = folder?.host ?? '';\n\n    return AlertDialog(\n      title: Text(localizations.newFolder, style: const TextStyle(fontSize: 16)),\n      content: Column(mainAxisSize: MainAxisSize.min, children: [\n        Row(children: [\n          SizedBox(width: 55, child: Text(localizations.enable)),\n          SwitchWidget(scale: 0.8, value: enabled, onChanged: (value) => enabled = value)\n        ]),\n        SizedBox(height: 10),\n        Row(children: [\n          SizedBox(width: 55, child: Text(localizations.name)),\n          Expanded(\n              child: TextFormField(\n                  minLines: 1,\n                  maxLines: 3,\n                  initialValue: name,\n                  onChanged: (val) => name = val,\n                  decoration: InputDecoration(border: OutlineInputBorder())))\n        ])\n      ]),\n      actions: [\n        TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n        TextButton(\n            onPressed: () {\n              HostsItem item;\n              if (folder == null) {\n                item = HostsItem(isFolder: true, host: name, enabled: enabled);\n                hostsManager.addHosts(item);\n              } else {\n                folder!.enabled = enabled;\n                folder!.host = name;\n                item = folder!;\n              }\n              Navigator.pop(context, item);\n            },\n            child: Text(localizations.save)),\n      ],\n    );\n  }\n}\n\nclass HostsEditDialog extends StatefulWidget {\n  final HostsItem? item;\n  final HostsItem? parent;\n\n  const HostsEditDialog({super.key, this.item, this.parent});\n\n  @override\n  State<HostsEditDialog> createState() => _HostsEditDialogState();\n}\n\nclass _HostsEditDialogState extends State<HostsEditDialog> {\n  GlobalKey formKey = GlobalKey<FormState>();\n\n  bool enabled = true;\n  TextEditingController hostController = TextEditingController();\n  TextEditingController toAddressController = TextEditingController();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.item != null) {\n      enabled = widget.item!.enabled;\n      hostController.text = widget.item!.host;\n      toAddressController.text = widget.item!.toAddress ?? '';\n    }\n  }\n\n  @override\n  void dispose() {\n    hostController.dispose();\n    toAddressController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n        contentPadding: const EdgeInsets.only(left: 20, right: 20, top: 10),\n        actions: [\n          TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n          TextButton(\n              onPressed: () {\n                if (!(formKey.currentState as FormState).validate()) {\n                  FlutterToastr.show(\n                      \"${localizations.domain} ${localizations.toAddress} ${localizations.cannotBeEmpty}\", context,\n                      position: FlutterToastr.center);\n                  return;\n                }\n\n                HostsItem? hostItem;\n                if (widget.item == null) {\n                  hostItem = HostsItem(\n                      enabled: enabled,\n                      parent: widget.parent?.id,\n                      host: hostController.text,\n                      toAddress: toAddressController.text);\n                  HostsManager.instance.then((it) => it.addHosts(hostItem!));\n                } else {\n                  widget.item!.enabled = enabled;\n                  widget.item!.host = hostController.text;\n                  widget.item!.toAddress = toAddressController.text;\n                  hostItem = widget.item;\n                }\n\n                Navigator.pop(context, hostItem);\n              },\n              child: Text(localizations.save)),\n        ],\n        content: Form(\n            key: formKey,\n            child: Column(mainAxisSize: MainAxisSize.min, children: [\n              Row(children: [\n                SizedBox(width: 80, child: Text(localizations.enable)),\n                Expanded(child: SwitchWidget(scale: 0.8, value: enabled, onChanged: (value) => enabled = value)),\n              ]),\n              const SizedBox(height: 8),\n              Row(children: [\n                SizedBox(width: 80, child: Text(localizations.domain)),\n                Expanded(\n                    child: TextFormField(\n                        controller: hostController,\n                        validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,\n                        decoration: const InputDecoration(\n                            hintText: '*.example.com',\n                            hintStyle: TextStyle(color: Colors.grey),\n                            errorStyle: TextStyle(height: 0, fontSize: 0),\n                            border: OutlineInputBorder()))),\n              ]),\n              const SizedBox(height: 10),\n              Row(children: [\n                SizedBox(width: 80, child: Text(localizations.toAddress)),\n                Expanded(\n                    child: TextFormField(\n                        controller: toAddressController,\n                        validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,\n                        decoration: const InputDecoration(\n                            hintText: '202.108.22.5',\n                            errorStyle: TextStyle(height: 0, fontSize: 0),\n                            hintStyle: TextStyle(color: Colors.grey),\n                            border: OutlineInputBorder()))),\n              ]),\n            ])));\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/preference.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/ui/mobile/setting/theme.dart';\n\n///设置\n///@author wanghongen\nclass Preference extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final AppConfiguration appConfiguration;\n\n  const Preference({super.key, required this.proxyServer, required this.appConfiguration});\n\n  @override\n  State<StatefulWidget> createState() => _PreferenceState();\n}\n\nclass _PreferenceState extends State<Preference> {\n  late ProxyServer proxyServer;\n  late Configuration configuration;\n  late AppConfiguration appConfiguration;\n\n  final memoryCleanupController = TextEditingController();\n  final memoryCleanupList = [null, 512, 1024, 2048, 4096];\n\n  @override\n  void initState() {\n    super.initState();\n    proxyServer = widget.proxyServer;\n    configuration = widget.proxyServer.configuration;\n    appConfiguration = widget.appConfiguration;\n\n    if (!memoryCleanupList.contains(appConfiguration.memoryCleanupThreshold)) {\n      memoryCleanupController.text = appConfiguration.memoryCleanupThreshold.toString();\n    }\n  }\n\n  @override\n  void dispose() {\n    memoryCleanupController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    final borderColor = Theme.of(context).dividerColor.withValues(alpha: 0.13);\n    final dividerColor = Theme.of(context).dividerColor.withValues(alpha: 0.22);\n\n    Widget section(List<Widget> tiles) => Card(\n          color: Colors.transparent,\n          elevation: 0,\n          shape: RoundedRectangleBorder(\n              side: BorderSide(color: borderColor),\n              borderRadius: BorderRadius.circular(10)),\n          child: Column(children: tiles),\n        );\n\n    return Scaffold(\n        appBar: AppBar(title: Text(localizations.preference, style: const TextStyle(fontSize: 16)), centerTitle: true),\n        body: ListView(\n          padding: const EdgeInsets.all(12),\n          children: [\n            section([\n              ListTile(\n                title: Text(localizations.language),\n                trailing: const Icon(Icons.arrow_forward_ios, size: 16),\n                onTap: () => _language(context),\n              ),\n              Divider(height: 0, thickness: 0.3, color: dividerColor),\n              MobileThemeSetting(appConfiguration: appConfiguration),\n              Divider(height: 0, thickness: 0.3, color: dividerColor),\n              ListTile(title: Text(localizations.themeColor)),\n              Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8),\n                  child: themeColor(context)),\n            ]),\n            const SizedBox(height: 12),\n            section([\n              ListTile(\n                  title: Text(localizations.autoStartup),\n                  subtitle: Text(localizations.autoStartupDescribe, style: const TextStyle(fontSize: 12)),\n                  trailing: SwitchWidget(\n                      value: proxyServer.configuration.startup,\n                      scale: 0.8,\n                      onChanged: (value) {\n                        configuration.startup = value;\n                        configuration.flushConfig();\n                      })),\n              Divider(height: 0, thickness: 0.3, color: dividerColor),\n              if (Platform.isAndroid) ...[\n                ListTile(\n                    title: Text(localizations.windowMode),\n                    subtitle: Text(localizations.windowModeSubTitle, style: const TextStyle(fontSize: 12)),\n                    trailing: SwitchWidget(\n                        value: appConfiguration.pipEnabled.value,\n                        scale: 0.8,\n                        onChanged: (value) {\n                          appConfiguration.pipEnabled.value = value;\n                          appConfiguration.flushConfig();\n                        })),\n                Divider(height: 0, thickness: 0.3, color: dividerColor),\n              ],\n              ListTile(\n                  title: Text(localizations.pipIcon),\n                  subtitle: Text(localizations.pipIconDescribe, style: const TextStyle(fontSize: 12)),\n                  trailing: SwitchWidget(\n                      value: appConfiguration.pipIcon.value,\n                      scale: 0.8,\n                      onChanged: (value) {\n                        appConfiguration.pipIcon.value = value;\n                        appConfiguration.flushConfig();\n                      })),\n              Divider(height: 0, thickness: 0.3, color: dividerColor),\n              ListTile(\n                  title: Text(localizations.headerExpanded),\n                  subtitle: Text(localizations.headerExpandedSubtitle, style: const TextStyle(fontSize: 12)),\n                  trailing: SwitchWidget(\n                      value: appConfiguration.headerExpanded,\n                      scale: 0.8,\n                      onChanged: (value) {\n                        appConfiguration.headerExpanded = value;\n                        appConfiguration.flushConfig();\n                      })),\n              Divider(height: 0, thickness: 0.3, color: dividerColor),\n              ListTile(\n                  title: Text(localizations.bottomNavigation),\n                  subtitle: Text(localizations.bottomNavigationSubtitle, style: const TextStyle(fontSize: 12)),\n                  trailing: SwitchWidget(\n                      value: appConfiguration.bottomNavigation,\n                      scale: 0.8,\n                      onChanged: (value) {\n                        appConfiguration.bottomNavigation = value;\n                        appConfiguration.flushConfig();\n                      })),\n            ]),\n            const SizedBox(height: 12),\n            section([\n              ListTile(\n                  title: Text(localizations.memoryCleanup),\n                  subtitle: Text(localizations.memoryCleanupSubtitle, style: const TextStyle(fontSize: 12)),\n                  trailing: memoryCleanup(context, localizations)),\n            ]),\n            const SizedBox(height: 15),\n          ],\n        ));\n  }\n\n  Widget themeColor(BuildContext context) {\n    return Wrap(\n      children: ColorMapping.colors.entries.map((pair) {\n        var dividerColor = Theme.of(context).focusColor;\n        var background = appConfiguration.themeColor == pair.value ? dividerColor : Colors.transparent;\n\n        return GestureDetector(\n            onTap: () => appConfiguration.setThemeColor = pair.key,\n            child: Tooltip(\n              message: pair.key,\n              child: Container(\n                margin: const EdgeInsets.all(4.0),\n                decoration: BoxDecoration(\n                  color: background,\n                  border: Border.all(color: Colors.transparent, width: 8),\n                ),\n                child: Dot(color: pair.value, size: 15),\n              ),\n            ));\n      }).toList(),\n    );\n  }\n\n  //选择语言\n  void _language(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    showDialog(\n        context: context,\n        builder: (context) {\n          return AlertDialog(\n            contentPadding: const EdgeInsets.only(left: 5, top: 5),\n            actionsPadding: const EdgeInsets.only(bottom: 5, right: 5),\n            title: Text(localizations.language, style: const TextStyle(fontSize: 16)),\n            content: Wrap(\n              children: [\n                TextButton(\n                    onPressed: () {\n                      appConfiguration.language = null;\n                      Navigator.of(context).pop();\n                    },\n                    child: Text(localizations.followSystem)),\n                const Divider(thickness: 0.5, height: 0),\n                TextButton(\n                    onPressed: () {\n                      appConfiguration.language = const Locale.fromSubtags(languageCode: 'zh');\n                      Navigator.of(context).pop();\n                    },\n                    child: const Text(\"简体中文\")),\n                const Divider(thickness: 0.5, height: 0),\n                TextButton(\n                    onPressed: () {\n                      appConfiguration.language = const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n                      Navigator.of(context).pop();\n                    },\n                    child: const Text(\"繁體中文\")),\n                const Divider(thickness: 0.5, height: 0),\n                TextButton(\n                    child: const Text(\"English\"),\n                    onPressed: () {\n                      appConfiguration.language = const Locale.fromSubtags(languageCode: 'en');\n                      Navigator.of(context).pop();\n                    }),\n                const Divider(thickness: 0.5),\n              ],\n            ),\n            actions: [\n              TextButton(\n                  onPressed: () {\n                    Navigator.of(context).pop();\n                  },\n                  child: Text(localizations.cancel)),\n            ],\n          );\n        });\n  }\n\n  bool memoryCleanupOpened = false;\n\n  ///内存清理\n  Widget memoryCleanup(BuildContext context, AppLocalizations localizations) {\n    try {\n      return DropdownButton<int>(\n          value: appConfiguration.memoryCleanupThreshold,\n          onTap: () => memoryCleanupOpened = true,\n          onChanged: (val) {\n            memoryCleanupOpened = false;\n            setState(() {\n              appConfiguration.memoryCleanupThreshold = val;\n            });\n            appConfiguration.flushConfig();\n          },\n          underline: Container(),\n          items: [\n            DropdownMenuItem(value: null, child: Text(localizations.unlimited)),\n            const DropdownMenuItem(value: 512, child: Text(\"512M\")),\n            const DropdownMenuItem(value: 1024, child: Text(\"1024M\")),\n            const DropdownMenuItem(value: 2048, child: Text(\"2048M\")),\n            const DropdownMenuItem(value: 4096, child: Text(\"4096M\")),\n            DropdownMenuInputItem(\n                controller: memoryCleanupController,\n                child: Container(\n                    constraints: BoxConstraints(maxWidth: 65, minWidth: 35),\n                    child: TextField(\n                        controller: memoryCleanupController,\n                        keyboardType: TextInputType.datetime,\n                        onSubmitted: (value) {\n                          setState(() {});\n                          appConfiguration.memoryCleanupThreshold = int.tryParse(value);\n                          appConfiguration.flushConfig();\n\n                          if (memoryCleanupOpened) {\n                            memoryCleanupOpened = false;\n                            Navigator.pop(context);\n                            return;\n                          }\n                        },\n                        inputFormatters: [\n                          LengthLimitingTextInputFormatter(5),\n                          FilteringTextInputFormatter.allow(RegExp(\"[0-9]\"))\n                        ],\n                        decoration: InputDecoration(hintText: localizations.custom, suffixText: \"M\")))),\n          ]);\n    } catch (e) {\n      appConfiguration.memoryCleanupThreshold = null;\n      logger.e('memory button build error', error: e, stackTrace: StackTrace.current);\n      return const SizedBox();\n    }\n  }\n}\n\nclass DropdownMenuInputItem extends DropdownMenuItem<int> {\n  final TextEditingController controller;\n\n  @override\n  int? get value => int.tryParse(controller.text) ?? 0;\n\n  const DropdownMenuInputItem({super.key, required this.controller, required super.child});\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/proxy.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\nclass ExternalProxyDialog extends StatefulWidget {\n  final Configuration configuration;\n\n  const ExternalProxyDialog({super.key, required this.configuration});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _ExternalProxyDialogState();\n  }\n}\n\nclass _ExternalProxyDialogState extends State<ExternalProxyDialog> {\n  final formKey = GlobalKey<FormState>();\n  late ProxyInfo externalProxy;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    externalProxy = ProxyInfo();\n    if (widget.configuration.externalProxy != null) {\n      externalProxy = ProxyInfo.fromJson(widget.configuration.externalProxy!.toJson());\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return AlertDialog(\n        scrollable: true,\n        title: Text(localizations.externalProxy, style: const TextStyle(fontSize: 15)),\n        actions: [\n          TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(localizations.cancel)),\n          TextButton(\n              onPressed: () async {\n                if (!formKey.currentState!.validate()) {\n                  return;\n                }\n                submit();\n              },\n              child: Text(localizations.confirm))\n        ],\n        content: Form(\n            key: formKey,\n            child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [\n              const SizedBox(height: 10),\n              Row(children: [\n                Expanded(flex: 2, child: Text(\"${localizations.enable}：\")),\n                Expanded(\n                    child: SwitchWidget(\n                  value: externalProxy.enabled,\n                  scale: 0.85,\n                  onChanged: (val) {\n                    setState(() => externalProxy.enabled = val);\n                  },\n                ))\n              ]),\n              Row(children: [\n                Expanded(flex: 2, child: Text(localizations.mobileDisplayPacketCapture)),\n                Expanded(\n                    child: SwitchWidget(\n                  value: externalProxy.capturePacket,\n                  scale: 0.85,\n                  onChanged: (val) {\n                    setState(() => externalProxy.capturePacket = val);\n                  },\n                ))\n              ]),\n              const SizedBox(height: 3),\n              Text(localizations.externalProxyServer, style: const TextStyle(fontWeight: FontWeight.w500)),\n              const SizedBox(height: 10),\n              SizedBox(\n                  height: 36,\n                  child: Row(children: [\n                    Expanded(\n                        child: TextFormField(\n                      initialValue: externalProxy.host,\n                      validator: (val) => val == null || val.isEmpty ? localizations.cannotBeEmpty : null,\n                      onChanged: (val) => externalProxy.host = val,\n                      decoration: const InputDecoration(\n                        contentPadding: EdgeInsets.symmetric(horizontal: 8),\n                        hintText: 'Host',\n                        hintStyle: TextStyle(color: Colors.grey),\n                        border: OutlineInputBorder(),\n                      ),\n                    )),\n                    const SizedBox(child: Text(' : ', style: TextStyle(fontSize: 22))),\n                    SizedBox(\n                        width: 68,\n                        child: TextFormField(\n                          initialValue: externalProxy.port?.toString() ?? '',\n                          keyboardType: TextInputType.datetime,\n                          inputFormatters: <TextInputFormatter>[\n                            LengthLimitingTextInputFormatter(5),\n                            FilteringTextInputFormatter.allow(RegExp(\"[0-9]\"))\n                          ],\n                          onChanged: (val) => externalProxy.port = int.parse(val),\n                          validator: (val) => val == null || val.isEmpty ? localizations.cannotBeEmpty : null,\n                          decoration: const InputDecoration(\n                            contentPadding: EdgeInsets.symmetric(horizontal: 8),\n                            hintText: 'Port',\n                            hintStyle: TextStyle(color: Colors.grey),\n                            border: OutlineInputBorder(),\n                          ),\n                        ))\n                  ])),\n\n              //认证\n              const SizedBox(height: 15),\n              Text(localizations.externalProxyAuth, style: const TextStyle(fontWeight: FontWeight.w500)),\n              const SizedBox(height: 10),\n              SizedBox(\n                  height: 36,\n                  child: Row(children: [\n                    SizedBox(\n                        width: isCN ? 65 : 85,\n                        child: Text('${localizations.username}：', style: const TextStyle(fontWeight: FontWeight.w300))),\n                    Expanded(\n                        child: TextFormField(\n                          initialValue: externalProxy.username,\n                          onChanged: (val) => externalProxy.username = val,\n                          decoration: const InputDecoration(\n                            contentPadding: EdgeInsets.symmetric(horizontal: 8),\n                            border: OutlineInputBorder(),\n                          ),\n                        ))\n                  ])),\n              const SizedBox(height: 10),\n\n              SizedBox(\n                  height: 36,\n                  child: Row(children: [\n                    SizedBox(\n                        width: isCN ? 65 : 85,\n                        child: Text('${localizations.password}：', style: const TextStyle(fontWeight: FontWeight.w300))),\n                    Expanded(\n                        child: TextFormField(\n                          initialValue: externalProxy.password,\n                          onChanged: (val) => externalProxy.password = val,\n                          decoration: const InputDecoration(\n                            contentPadding: EdgeInsets.symmetric(horizontal: 8),\n                            border: OutlineInputBorder(),\n                          ),\n                        ))\n                  ])),\n            ])));\n  }\n\n  submit() async {\n    bool setting = true;\n    if (externalProxy.enabled) {\n      try {\n        var socket = await Socket.connect(externalProxy.host, externalProxy.port!, timeout: const Duration(seconds: 1));\n        socket.destroy();\n      } on SocketException catch (_) {\n        setting = false;\n        if (mounted) {\n          await showDialog(\n              context: context,\n              builder: (_) => AlertDialog(\n                    title: Text(localizations.externalProxyConnectFailure),\n                    content: Text(localizations.externalProxyFailureConfirm, style: const TextStyle(fontSize: 12)),\n                    actions: [\n                      TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(localizations.cancel)),\n                      TextButton(\n                          onPressed: () {\n                            setting = true;\n                            Navigator.of(context).pop();\n                          },\n                          child: Text(localizations.confirm))\n                    ],\n                  ));\n        }\n      }\n    }\n\n    if (setting) {\n      widget.configuration.externalProxy = externalProxy;\n      widget.configuration.flushConfig();\n    }\n\n    if (mounted) Navigator.of(context).pop();\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/report_servers.dart",
    "content": "/*\n * Mobile report servers page\n */\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/components/manager/report_server_manager.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport '../../../l10n/app_localizations.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass ReportServersPageMobile extends StatefulWidget {\n  const ReportServersPageMobile({super.key});\n\n  @override\n  State<ReportServersPageMobile> createState() => _ReportServersPageMobileState();\n}\n\nclass _ReportServersPageMobileState extends State<ReportServersPageMobile> {\n  List<ReportServer> _servers = [];\n  bool _loading = true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  Future<void> _openGuide() async {\n    final locale = AppLocalizations.of(context)?.localeName ?? '';\n    final cn = 'https://gitee.com/wanghongenpin/proxypin/wikis/%E4%B8%8A%E6%8A%A5%E6%9C%8D%E5%8A%A1%E5%99%A8';\n    final en = 'https://github.com/wanghongenpin/proxypin/wiki/Report-Server';\n    final url = (locale.startsWith('zh')) ? cn : en;\n    final uri = Uri.parse(url);\n    try {\n      if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {\n        FlutterToastr.show('Open guide failed', context);\n      }\n    } catch (e) {\n      FlutterToastr.show('Open guide failed: $e', context);\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _load();\n  }\n\n  Future<void> _load() async {\n    final manager = await ReportServerManager.instance;\n    setState(() {\n      _servers = List.of(manager.servers);\n      _loading = false;\n    });\n  }\n\n  Future<ReportServer?> _showServerDialog({ReportServer? initial}) async {\n    // Push the edit page and return the created/edited ReportServer\n    final result = await Navigator.of(context).push<ReportServer>(\n      MaterialPageRoute(\n        builder: (ctx) => ReportServerEditPageMobile(initial: initial),\n      ),\n    );\n\n    return result;\n  }\n\n  Future<void> _addServer() async {\n    final server = await _showServerDialog();\n    if (server != null) {\n      final manager = await ReportServerManager.instance;\n      await manager.add(server);\n      await _load();\n    }\n  }\n\n  Future<void> _editServer(int index) async {\n    final initial = _servers[index];\n    final server = await _showServerDialog(initial: initial);\n    if (server != null) {\n      final manager = await ReportServerManager.instance;\n      await manager.update(index, server);\n      await _load();\n    }\n  }\n\n  Future<void> _confirmDelete(int index) async {\n    showConfirmDialog(context, onConfirm: () async {\n      final manager = await ReportServerManager.instance;\n      await manager.removeAt(index);\n      await _load();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(localizations.reportServers, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n        centerTitle: true,\n        actions: [\n          IconButton(\n            tooltip: localizations.useGuide,\n            onPressed: _openGuide,\n            icon: const Icon(Icons.help_outline, size: 22),\n          ),\n          IconButton(\n            tooltip: localizations.add,\n            onPressed: _addServer,\n            icon: const Icon(Icons.add, size: 26),\n          ),\n          SizedBox(width: 5)\n        ],\n      ),\n      body: _loading\n          ? const Center(child: CircularProgressIndicator())\n          : _servers.isEmpty\n              ? Center(child: Text(localizations.emptyData))\n              : ListView.separated(\n                  itemCount: _servers.length,\n                  separatorBuilder: (_, __) => const Divider(height: 0, thickness: 0.3),\n                  itemBuilder: (ctx, idx) {\n                    final s = _servers[idx];\n                    return ListTile(\n                      contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),\n                      leading: SizedBox(\n                          width: 32,\n                          child: Checkbox(\n                              value: s.enabled,\n                              onChanged: (v) async {\n                                final manager = await ReportServerManager.instance;\n                                await manager.toggleEnabled(idx, v == true);\n                                await _load();\n                              })),\n                      title: Text(s.name.isEmpty ? '-' : s.name),\n                      subtitle: Text(s.serverUrl),\n                      trailing: Row(\n                        mainAxisSize: MainAxisSize.min,\n                        children: [\n                          // IconButton(\n                          //     onPressed: () => _editServer(idx), icon: const Icon(Icons.edit_outlined, size: 23)),\n                          IconButton(\n                              onPressed: () => _confirmDelete(idx), icon: const Icon(Icons.delete_outline, size: 23)),\n                        ],\n                      ),\n                      onTap: () => _editServer(idx),\n                    );\n                  },\n                ),\n    );\n  }\n}\n\n// A standalone page for adding / editing a ReportServer on mobile.\nclass ReportServerEditPageMobile extends StatefulWidget {\n  final ReportServer? initial;\n\n  const ReportServerEditPageMobile({super.key, this.initial});\n\n  @override\n  State<ReportServerEditPageMobile> createState() => _ReportServerEditPageMobileState();\n}\n\nclass _ReportServerEditPageMobileState extends State<ReportServerEditPageMobile> {\n  late TextEditingController _nameCtrl;\n  late TextEditingController _matchUrlCtrl;\n  late TextEditingController _serverUrlCtrl;\n  String _compression = 'none';\n  bool _enabled = true;\n\n  final _formKey = GlobalKey<FormState>();\n\n  @override\n  void initState() {\n    super.initState();\n    final init = widget.initial;\n    _nameCtrl = TextEditingController(text: init?.name ?? '');\n    _matchUrlCtrl = TextEditingController(text: init?.matchUrl ?? '');\n    _serverUrlCtrl = TextEditingController(text: init?.serverUrl ?? '');\n    _compression = init?.compression ?? 'none';\n    _enabled = init?.enabled ?? true;\n  }\n\n  InputDecoration dec({String? hint}) => InputDecoration(\n        hintText: hint,\n        hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n        contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),\n        focusedBorder:\n            OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)),\n        isDense: true,\n        border: const OutlineInputBorder(),\n      );\n\n  Widget labeled(String label, Widget field, {bool expanded = true}) => Row(\n        children: [\n          SizedBox(width: AppLocalizations.of(context)!.localeName == 'en' ? 95 : 85, child: Text(label)),\n          const SizedBox(width: 12),\n          expanded ? Expanded(child: field) : field,\n        ],\n      );\n\n  void _onSave() {\n    if (!(_formKey.currentState as FormState).validate()) {\n      FlutterToastr.show(\n          \"${AppLocalizations.of(context)!.serverUrl} ${AppLocalizations.of(context)!.cannotBeEmpty}\", context,\n          position: FlutterToastr.top);\n      return;\n    }\n\n    var serverUrl = _serverUrlCtrl.text.trim();\n    if (!serverUrl.startsWith('http://') && !serverUrl.startsWith('https://')) {\n      serverUrl = 'http://$serverUrl';\n    }\n\n    final server = ReportServer(\n      name: _nameCtrl.text.trim(),\n      matchUrl: _matchUrlCtrl.text.trim(),\n      serverUrl: serverUrl,\n      enabled: _enabled,\n      compression: _compression,\n    );\n\n    Navigator.of(context).pop(server);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final localizations = AppLocalizations.of(context)!;\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(widget.initial == null ? localizations.addReportServer : localizations.editReportServer),\n        centerTitle: true,\n        actions: [\n          TextButton(onPressed: _onSave, child: Text(localizations.save)),\n        ],\n      ),\n      body: Padding(\n        padding: const EdgeInsets.all(12.0),\n        child: Form(\n          key: _formKey,\n          child: SingleChildScrollView(\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                const SizedBox(height: 6),\n                labeled('${localizations.name}: ',\n                    TextField(controller: _nameCtrl, decoration: dec(hint: localizations.pleaseEnter))),\n                const SizedBox(height: 12),\n                labeled(\n                  '${localizations.match} URL: ',\n                  TextFormField(\n                      controller: _matchUrlCtrl,\n                      keyboardType: TextInputType.url,\n                      validator: (v) => v?.isNotEmpty == true ? null : \"\",\n                      decoration: dec(hint: 'https://example.com/api/*')),\n                ),\n                const SizedBox(height: 12),\n                labeled(\n                  '${localizations.serverUrl}: ',\n                  TextFormField(\n                      controller: _serverUrlCtrl,\n                      keyboardType: TextInputType.url,\n                      validator: (v) => v?.isNotEmpty == true ? null : \"\",\n                      decoration: dec(hint: 'http://example.com/report')),\n                ),\n                const SizedBox(height: 12),\n                labeled(\n                  '${localizations.compression}: ',\n                  expanded: false,\n                  SizedBox(\n                    width: 120,\n                    child: DropdownButtonFormField<String>(\n                      value: _compression,\n                      decoration: dec(),\n                      items: [\n                        DropdownMenuItem(value: 'none', child: Text(localizations.compressionNone)),\n                        DropdownMenuItem(value: 'gzip', child: Text('GZIP')),\n                      ],\n                      onChanged: (v) => setState(() => _compression = v ?? 'none'),\n                    ),\n                  ),\n                ),\n                const SizedBox(height: 12),\n                labeled(\n                  '${localizations.enable}: ',\n                  Align(\n                      alignment: Alignment.centerLeft,\n                      child: SwitchWidget(value: _enabled, scale: 0.9, onChanged: (v) => setState(() => _enabled = v))),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/request_block.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_block_manager.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nclass MobileRequestBlock extends StatefulWidget {\n  final RequestBlockManager requestBlockManager;\n\n  const MobileRequestBlock({super.key, required this.requestBlockManager});\n\n  @override\n  State<MobileRequestBlock> createState() => _RequestBlockState();\n}\n\nclass _RequestBlockState extends State<MobileRequestBlock> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        resizeToAvoidBottomInset: false,\n        appBar: AppBar(title: Text(localizations.requestBlock, style: const TextStyle(fontSize: 16))),\n        body: Container(\n            padding: const EdgeInsets.all(10),\n            child: Wrap(children: [\n              Row(children: [\n                const SizedBox(width: 8),\n                Text(localizations.enable),\n                const SizedBox(width: 10),\n                SwitchWidget(\n                    scale: 0.8,\n                    value: widget.requestBlockManager.enabled,\n                    onChanged: (value) {\n                      widget.requestBlockManager.enabled = value;\n                      widget.requestBlockManager.flushConfig();\n                    }),\n                const Expanded(child: SizedBox()),\n                TextButton.icon(\n                    icon: const Icon(Icons.add, size: 20), onPressed: showEdit, label: Text(localizations.add)),\n                const SizedBox(width: 5),\n              ]),\n              const SizedBox(height: 10),\n              Container(\n                  height: 600,\n                  decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n                  child: Column(children: [\n                    const SizedBox(height: 5),\n                    Row(\n                      children: [\n                        Container(width: 15),\n                        const Expanded(child: Text('URL', style: TextStyle(fontSize: 14))),\n                        SizedBox(width: 60, child: Text(localizations.enable, style: const TextStyle(fontSize: 14))),\n                        SizedBox(width: 75, child: Text(localizations.action, style: const TextStyle(fontSize: 14))),\n                      ],\n                    ),\n                    const Divider(thickness: 0.5),\n                    Expanded(\n                        child: ListView.builder(\n                            itemCount: widget.requestBlockManager.list.length, itemBuilder: (_, index) => row(index)))\n                  ]))\n            ])));\n  }\n\n  Widget row(int index) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n    bool isCN = localizations.localeName == 'zh';\n    var list = widget.requestBlockManager.list;\n\n    return InkWell(\n        highlightColor: Colors.transparent,\n        splashColor: Colors.transparent,\n        hoverColor: primaryColor.withOpacity(0.3),\n        onLongPress: () => showMenus(index),\n        onTap: () => showEdit(index),\n        child: Container(\n            color: index.isEven ? Colors.grey.withOpacity(0.1) : null,\n            height: 38,\n            padding: const EdgeInsets.symmetric(vertical: 3),\n            child: Row(\n              children: [\n                const SizedBox(width: 10),\n                Expanded(child: Text(list[index].url.fixAutoLines(), style: const TextStyle(fontSize: 13))),\n                const SizedBox(width: 5),\n                SwitchWidget(\n                    scale: 0.65,\n                    value: list[index].enabled,\n                    onChanged: (val) {\n                      list[index].enabled = val;\n                      setState(() {\n                        widget.requestBlockManager.flushConfig();\n                      });\n                    }),\n                const SizedBox(width: 5),\n                SizedBox(\n                    width: 85,\n                    child: Text(isCN ? list[index].type.label : list[index].type.name,\n                        style: const TextStyle(fontSize: 13)))\n              ],\n            )));\n  }\n\n  //点击菜单\n  showMenus(int index) {\n    var list = widget.requestBlockManager.list;\n\n    showModalBottomSheet(\n        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),\n        context: context,\n        isScrollControlled: true,\n        enableDrag: true,\n        builder: (ctx) {\n          return Wrap(children: [\n            BottomSheetItem(text: localizations.edit, onPressed: () => showEdit(index)),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: list[index].enabled ? localizations.disabled : localizations.enable,\n                onPressed: () {\n                  list[index].enabled = !list[index].enabled;\n                  setState(() {\n                    widget.requestBlockManager.flushConfig();\n                  });\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: localizations.delete,\n                onPressed: () async {\n                  await widget.requestBlockManager.removeBlockRequest(index);\n                  setState(() {});\n                }),\n            Container(color: Theme.of(context).hoverColor, height: 8),\n            TextButton(\n                child: Container(\n                    height: 50,\n                    width: double.infinity,\n                    padding: const EdgeInsets.only(top: 10),\n                    child: Text(localizations.cancel, textAlign: TextAlign.center)),\n                onPressed: () {\n                  Navigator.of(context).pop();\n                }),\n          ]);\n        });\n  }\n\n  showEdit([int? index]) {\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (BuildContext context) {\n          return RequestBlockAddDialog(requestBlockManager: widget.requestBlockManager, index: index);\n        }).then((value) {\n      if (value != null) {\n        setState(() {});\n      }\n    });\n  }\n}\n\nclass RequestBlockAddDialog extends StatelessWidget {\n  final RequestBlockManager requestBlockManager;\n  final int? index;\n\n  const RequestBlockAddDialog({super.key, required this.requestBlockManager, this.index});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    bool isCN = localizations.localeName == 'zh';\n\n    GlobalKey formKey = GlobalKey<FormState>();\n    RequestBlockItem item =\n        index == null ? RequestBlockItem(true, '', BlockType.values.first) : requestBlockManager.list.elementAt(index!);\n    bool enabled = item.enabled;\n    return AlertDialog(\n        scrollable: true,\n        content: Padding(\n            padding: const EdgeInsets.all(8.0),\n            child: Form(\n                key: formKey,\n                child: Column(children: <Widget>[\n                  SwitchWidget(title: localizations.enable, value: item.enabled, onChanged: (val) => enabled = val),\n                  const SizedBox(height: 10),\n                  TextFormField(\n                      initialValue: item.url,\n                      maxLines: 3,\n                      minLines: 1,\n                      decoration: const InputDecoration(\n                          isDense: true,\n                          labelText: 'URL',\n                          hintText: 'https://example.com/*',\n                          border: OutlineInputBorder()),\n                      validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,\n                      onSaved: (val) => item.url = val!.trim()),\n                  const SizedBox(height: 15),\n                  DropdownButtonFormField(\n                      value: item.type,\n                      decoration: InputDecoration(\n                          isDense: true, labelText: localizations.type, border: const OutlineInputBorder()),\n                      items: BlockType.values\n                          .map((e) => DropdownMenuItem(\n                              value: e, child: Text(isCN ? e.label : e.name, style: const TextStyle(fontSize: 14))))\n                          .toList(),\n                      onSaved: (val) => item.type = val!,\n                      onChanged: (val) {}),\n                ]))),\n        actions: [\n          TextButton(child: Text(localizations.cancel), onPressed: () => Navigator.of(context).pop()),\n          TextButton(\n              child: Text(localizations.save),\n              onPressed: () {\n                if (!(formKey.currentState as FormState).validate()) {\n                  return;\n                }\n                (formKey.currentState as FormState).save();\n\n                item.enabled = enabled;\n                item.urlReg = null;\n                if (index != null) {\n                  requestBlockManager.list[index!] = item;\n                } else {\n                  requestBlockManager.addBlockRequest(item);\n                }\n                requestBlockManager.flushConfig();\n                Navigator.of(context).pop(item);\n              }),\n        ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/request_breakpoint.dart",
    "content": "import 'dart:collection';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_breakpoint_manager.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\nimport '../../component/http_method_popup.dart';\n\nclass MobileRequestBreakpointPage extends StatefulWidget {\n  final RequestBreakpointManager manager;\n\n  const MobileRequestBreakpointPage({super.key, required this.manager});\n\n  @override\n  State<MobileRequestBreakpointPage> createState() => _RequestBreakpointPageState();\n}\n\nclass _RequestBreakpointPageState extends State<MobileRequestBreakpointPage> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n  List<RequestBreakpointRule> rules = [];\n  bool enabled = false;\n\n  RequestBreakpointManager get manager => widget.manager;\n\n  bool selectionMode = false;\n  final Set<int> selected = HashSet<int>();\n\n  Future<void> _save() async {\n    await manager.save();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    enabled = manager.enabled;\n    rules = manager.list;\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isEN = Localizations.localeOf(context).languageCode == 'en';\n\n    return Scaffold(\n        backgroundColor: Theme.of(context).dialogTheme.backgroundColor,\n        appBar: AppBar(\n            title: Text(localizations.breakpoint, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n            toolbarHeight: 36,\n            centerTitle: true),\n        body: Center(\n            child: Container(\n                padding: const EdgeInsets.only(left: 15, right: 10),\n                child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n                  Row(children: [\n                    SizedBox(\n                        width: isEN ? 230 : 160,\n                        child: ListTile(\n                            title: Text(\"${localizations.enable} ${localizations.breakpoint}\"),\n                            contentPadding: const EdgeInsets.only(left: 2),\n                            trailing: SwitchWidget(\n                                value: enabled,\n                                scale: 0.8,\n                                onChanged: (val) async {\n                                  manager.enabled = val;\n                                  await _save();\n                                  setState(() {\n                                    enabled = val;\n                                  });\n                                }))),\n                    const SizedBox(width: 10),\n                    Expanded(\n                        child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n                      IconButton(\n                          icon: Icon(Icons.add, size: 22, color: Theme.of(context).colorScheme.primary),\n                          onPressed: _editRule,\n                          tooltip: localizations.add),\n                      const SizedBox(width: 5),\n                      IconButton(\n                          icon: Icon(Icons.input_rounded, size: 22, color: Theme.of(context).colorScheme.primary),\n                          onPressed: _import,\n                          tooltip: localizations.import),\n                    ])),\n                    const SizedBox(width: 15)\n                  ]),\n                  const SizedBox(height: 10),\n                  Expanded(child: _buildList()),\n                  if (selectionMode) _buildSelectionFooter(),\n                ]))));\n  }\n\n  Widget _buildList() {\n    return Container(\n      padding: const EdgeInsets.only(top: 10),\n      decoration: BoxDecoration(border: Border.all(color: Colors.grey.withValues(alpha: 0.2))),\n      child: Column(\n        children: [\n          Padding(\n            padding: const EdgeInsets.only(left: 5, bottom: 5),\n            child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [\n              Container(width: 65, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)),\n              SizedBox(width: 45, child: Text(localizations.enable, textAlign: TextAlign.center)),\n              const VerticalDivider(width: 10),\n              Expanded(child: Text(\"URL\", textAlign: TextAlign.center)),\n              SizedBox(width: 100, child: Text(localizations.breakpoint, textAlign: TextAlign.center)),\n            ]),\n          ),\n          const Divider(thickness: 0.5, height: 5),\n          Expanded(\n            child: ListView.builder(\n              itemCount: rules.length,\n              itemBuilder: (context, index) => _buildRow(index),\n            ),\n          )\n        ],\n      ),\n    );\n  }\n\n  Widget _buildRow(int index) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n    var rule = rules[index];\n\n    return InkWell(\n      highlightColor: Colors.transparent,\n      splashColor: Colors.transparent,\n      hoverColor: primaryColor.withValues(alpha: 0.3),\n      onLongPress: () => _showRuleActions(index),\n      onTap: () {\n        if (selectionMode) {\n          setState(() {\n            if (!selected.add(index)) {\n              selected.remove(index);\n            }\n          });\n          return;\n        }\n        _editRule(rule: rule);\n      },\n      child: Container(\n        color: selected.contains(index)\n            ? primaryColor.withValues(alpha: 0.5)\n            : index.isEven\n                ? Colors.grey.withValues(alpha: 0.1)\n                : null,\n        height: 32,\n        padding: const EdgeInsets.all(5),\n        child: Row(children: [\n          SizedBox(\n            width: 65,\n            child: Text(rule.name ?? \"\",\n                overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),\n          ),\n          SizedBox(\n              width: 45,\n              child: SwitchWidget(\n                  scale: 0.65,\n                  value: rule.enabled,\n                  onChanged: (val) async {\n                    rule.enabled = val;\n                    await _save();\n                  })),\n          const SizedBox(width: 10),\n          Expanded(child: Text(rule.url, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center)),\n          SizedBox(\n              width: 100,\n              child: Text(\n                  \"${rule.interceptRequest ? localizations.request : \"\"}${rule.interceptRequest && rule.interceptResponse ? \"/\" : \"\"}${rule.interceptResponse ? localizations.response : \"\"}\",\n                  textAlign: TextAlign.center,\n                  overflow: TextOverflow.ellipsis)),\n        ]),\n      ),\n    );\n  }\n\n  Future<void> _export(RequestBreakpointManager? manager, {List<int>? indexes}) async {\n    try {\n      if (manager == null || manager.list.isEmpty) return;\n      final rules = manager.list;\n      final keys = (indexes == null || indexes.isEmpty)\n          ? List<int>.generate(rules.length, (i) => i)\n          : (indexes.toList()..sort());\n      final data = keys.map((i) => rules[i].toJson()).toList();\n      var bytes = utf8.encode(jsonEncode(data));\n      final path = await FilePicker.platform.saveFile(fileName: 'request_breakpoints.json', bytes: bytes);\n      if (path == null) return;\n      if (mounted) FlutterToastr.show(localizations.exportSuccess, context);\n    } catch (e) {\n      logger.e('导出失败', error: e);\n      if (mounted) FlutterToastr.show('Export failed: $e', context);\n    }\n  }\n\n  Future<void> _import() async {\n    try {\n      FilePickerResult? result = await FilePicker.platform.pickFiles(\n        type: FileType.custom,\n        allowedExtensions: ['json'],\n      );\n      if (result == null || result.files.isEmpty) return;\n      File file = File(result.files.single.path!);\n      String content = await file.readAsString();\n      List<dynamic> list = jsonDecode(content);\n      var newRules = list.map((e) => RequestBreakpointRule.fromJson(e)).toList();\n      for (var rule in newRules) {\n        manager.list.add(rule);\n      }\n      await _save();\n      setState(() {\n        rules = manager.list;\n      });\n\n      if (mounted) FlutterToastr.show(localizations.importSuccess, context);\n    } catch (e) {\n      logger.e('Import failed', error: e);\n      if (mounted) FlutterToastr.show(localizations.importFailed, context);\n    }\n  }\n\n  Stack _buildSelectionFooter() {\n    final l10n = localizations;\n    return Stack(children: [\n      Container(\n          height: 50,\n          width: double.infinity,\n          margin: const EdgeInsets.only(top: 10),\n          decoration: BoxDecoration(border: Border.all(color: Colors.grey.withValues(alpha: 0.2)))),\n      Positioned(\n          top: 0,\n          left: 0,\n          right: 0,\n          child: Center(\n              child: TextButton(\n                  onPressed: () {},\n                  child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [\n                    TextButton.icon(\n                        onPressed: selected.isEmpty\n                            ? null\n                            : () async {\n                                // export selected only\n                                final m = await RequestBreakpointManager.instance;\n                                await _export(m, indexes: selected.toList());\n                                setState(() {\n                                  selected.clear();\n                                  selectionMode = false;\n                                });\n                              },\n                        icon: const Icon(Icons.share, size: 18),\n                        label: Text(l10n.export, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: selected.isEmpty ? null : () => _removeSelected(),\n                        icon: const Icon(Icons.delete, size: 18),\n                        label: Text(l10n.delete, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () {\n                          setState(() {\n                            selectionMode = false;\n                            selected.clear();\n                          });\n                        },\n                        icon: const Icon(Icons.cancel, size: 18),\n                        label: Text(l10n.cancel, style: const TextStyle(fontSize: 14))),\n                  ]))))\n    ]);\n  }\n\n  void _showRuleActions(int index) {\n    final l10n = localizations;\n    setState(() {\n      selected.add(index);\n    });\n    showModalBottomSheet(\n        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),\n        context: context,\n        enableDrag: true,\n        builder: (ctx) {\n          return Wrap(children: [\n            BottomSheetItem(\n                text: l10n.multiple,\n                onPressed: () {\n                  setState(() => selectionMode = true);\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: l10n.edit,\n                onPressed: () {\n                  _editRule(rule: rules[index]);\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(text: l10n.export, onPressed: () => _export(manager, indexes: [index])),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: rules[index].enabled ? l10n.disabled : l10n.enable,\n                onPressed: () {\n                  rules[index].enabled = !rules[index].enabled;\n                  setState(() {});\n                  _save();\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: l10n.delete,\n                onPressed: () {\n                  _removeRule(index);\n                }),\n            Container(color: Theme.of(ctx).hoverColor, height: 8),\n            TextButton(\n                child: Container(\n                    height: 45,\n                    width: double.infinity,\n                    padding: const EdgeInsets.only(top: 10),\n                    child: Text(l10n.cancel, textAlign: TextAlign.center)),\n                onPressed: () {\n                  Navigator.of(ctx).pop();\n                }),\n          ]);\n        }).then((value) {\n      if (selectionMode) {\n        return;\n      }\n      setState(() {\n        selected.remove(index);\n      });\n    });\n  }\n\n  Future<void> _removeRule(int index) async {\n    showDialog(\n        context: context,\n        builder: (ctx) {\n          return AlertDialog(\n            title: Text(localizations.deleteHeaderConfirm, style: const TextStyle(fontSize: 18)),\n            actions: [\n              TextButton(onPressed: () => Navigator.pop(ctx), child: Text(localizations.cancel)),\n              TextButton(\n                  onPressed: () async {\n                    setState(() {\n                      rules.removeAt(index);\n                    });\n                    await _save();\n                    if (context.mounted) Navigator.pop(ctx);\n                  },\n                  child: Text(localizations.delete)),\n            ],\n          );\n        });\n  }\n\n  Future<void> _removeSelected() async {\n    showDialog(\n        context: context,\n        builder: (ctx) {\n          return AlertDialog(\n            title: Text(localizations.deleteHeaderConfirm, style: const TextStyle(fontSize: 18)),\n            actions: [\n              TextButton(onPressed: () => Navigator.pop(ctx), child: Text(localizations.cancel)),\n              TextButton(\n                  onPressed: () async {\n                    var list = selected.toList();\n                    list.sort((a, b) => b.compareTo(a));\n                    for (var i in list) {\n                      rules.removeAt(i);\n                    }\n                    setState(() {\n                      selected.clear();\n                      selectionMode = false;\n                    });\n                    await _save();\n                    if (context.mounted) Navigator.pop(ctx);\n                  },\n                  child: Text(localizations.delete)),\n            ],\n          );\n        });\n  }\n\n  void _editRule({RequestBreakpointRule? rule}) {\n    Navigator.push(\n      context,\n      MaterialPageRoute(\n        builder: (context) => MobileBreakpointRuleEditor(rule: rule),\n      ),\n    ).then((value) async {\n      if (value != null && value is RequestBreakpointRule) {\n        setState(() {\n          if (rule == null) {\n            rules.add(value);\n          }\n        });\n        await _save();\n      }\n    });\n  }\n}\n\nclass MobileBreakpointRuleEditor extends StatefulWidget {\n  final RequestBreakpointRule? rule;\n\n  const MobileBreakpointRuleEditor({super.key, this.rule});\n\n  @override\n  State<MobileBreakpointRuleEditor> createState() => _MobileBreakpointRuleEditorState();\n}\n\nclass _MobileBreakpointRuleEditorState extends State<MobileBreakpointRuleEditor> {\n  late RequestBreakpointRule rule;\n  final _formKey = GlobalKey<FormState>();\n\n  late TextEditingController nameInput;\n  late TextEditingController urlInput;\n\n  // Local state for methods to avoid modifying rule in-place before save\n  HttpMethod? _method;\n  bool _interceptRequest = true;\n  bool _interceptResponse = true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    rule = widget.rule ?? RequestBreakpointRule(url: '');\n    nameInput = TextEditingController(text: rule.name);\n    urlInput = TextEditingController(text: rule.url);\n    _method = rule.method;\n    _interceptRequest = rule.interceptRequest;\n    _interceptResponse = rule.interceptResponse;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n            title: Text(\n                widget.rule == null\n                    ? \"${localizations.add} ${localizations.breakpointRule}\"\n                    : \"${localizations.edit} ${localizations.breakpointRule}\",\n                style: const TextStyle(fontSize: 16)),\n            actions: [\n              TextButton(\n                  onPressed: () {\n                    if (!(_formKey.currentState?.validate() ?? false)) {\n                      return;\n                    }\n                    rule.name = nameInput.text;\n                    rule.url = urlInput.text;\n                    rule.method = _method;\n                    rule.interceptRequest = _interceptRequest;\n                    rule.interceptResponse = _interceptResponse;\n                    rule.enabled = true;\n                    Navigator.pop(context, rule);\n                  },\n                  child: Text(localizations.save))\n            ]),\n        body: Padding(\n            padding: const EdgeInsets.all(15),\n            child: Form(\n                key: _formKey,\n                child: ListView(children: [\n                  TextFormField(\n                    controller: nameInput,\n                    decoration: InputDecoration(labelText: localizations.name, border: const OutlineInputBorder()),\n                  ),\n                  const SizedBox(height: 15),\n                  TextFormField(\n                    controller: urlInput,\n                    validator: (val) => val?.isNotEmpty == true ? null : localizations.cannotBeEmpty,\n                    decoration: InputDecoration(\n                        labelText: 'URL',\n                        hintText: 'https://www.example.com/api/*',\n                        border: const OutlineInputBorder(),\n                        prefixIcon: Padding(\n                            padding: const EdgeInsets.symmetric(horizontal: 5),\n                            child: MethodPopupMenu(value: _method, onChanged: (val) => setState(() => _method = val)))),\n                  ),\n                  const SizedBox(height: 15),\n                  SwitchListTile(\n                      title: Text(localizations.request),\n                      value: _interceptRequest,\n                      onChanged: (val) => setState(() => _interceptRequest = val)),\n                  SwitchListTile(\n                      title: Text(localizations.response),\n                      value: _interceptResponse,\n                      onChanged: (val) => setState(() => _interceptResponse = val)),\n                ]))));\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/request_crypto.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\nimport 'dart:collection';\nimport 'dart:math' as math;\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_crypto_manager.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\n\nbool _refresh = false;\n\nFuture<void> _refreshConfig({bool force = false}) async {\n  if (force) {\n    _refresh = false;\n    await RequestCryptoManager.instance.then((manager) => manager.flushConfig());\n    return;\n  }\n\n  if (_refresh) return;\n  _refresh = true;\n  Future.delayed(const Duration(milliseconds: 800), () async {\n    _refresh = false;\n    await RequestCryptoManager.instance.then((manager) => manager.flushConfig());\n  });\n}\n\nclass MobileRequestCryptoPage extends StatefulWidget {\n  const MobileRequestCryptoPage({super.key});\n\n  @override\n  State<MobileRequestCryptoPage> createState() => _MobileRequestCryptoPageState();\n}\n\nclass _MobileRequestCryptoPageState extends State<MobileRequestCryptoPage> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  bool enabled = false;\n  bool selectionMode = false;\n  final Set<int> selected = HashSet<int>();\n  bool changed = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final l10n = localizations;\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(l10n.requestCrypto, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n        toolbarHeight: 36,\n        centerTitle: true,\n      ),\n      persistentFooterButtons: selectionMode ? [_buildSelectionFooter()] : null,\n      body: Padding(\n        padding: const EdgeInsets.all(10),\n        child: futureWidget(\n          RequestCryptoManager.instance,\n          loading: true,\n          (manager) {\n            enabled = manager.enabled;\n\n            return Column(\n              children: [\n                Row(\n                  children: [\n                    Text(\"${l10n.enable} ${l10n.requestCrypto}\"),\n                    const SizedBox(width: 8),\n                    SwitchWidget(\n                      value: enabled,\n                      scale: 0.8,\n                      onChanged: (val) {\n                        enabled = val;\n                        manager.enabled = val;\n                        changed = true;\n                        setState(() {});\n                        _refreshConfig();\n                      },\n                    ),\n                  ],\n                ),\n                Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n                  TextButton.icon(\n                    icon: const Icon(Icons.add, size: 20),\n                    onPressed: () => _addRule(manager),\n                    label: Text(l10n.add),\n                  ),\n                  const SizedBox(width: 5),\n                  TextButton.icon(\n                    icon: const Icon(Icons.input_rounded, size: 20),\n                    onPressed: () => _import(manager),\n                    label: Text(l10n.import),\n                  ),\n                ]),\n                const SizedBox(height: 10),\n                Expanded(child: _buildRuleList(manager)),\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _buildRuleList(RequestCryptoManager manager) {\n    final l10n = localizations;\n    final primaryColor = Theme.of(context).colorScheme.primary;\n    final rules = manager.rules;\n\n    return Scaffold(\n      body: Container(\n        padding: const EdgeInsets.only(top: 10, bottom: 30),\n        decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n        child: rules.isEmpty\n            ? const Center(child: Text('-'))\n            : Scrollbar(\n                child: ListView(children: [\n                Row(\n                  mainAxisAlignment: MainAxisAlignment.start,\n                  children: [\n                    Container(width: 70, padding: const EdgeInsets.only(left: 10), child: Text(l10n.name)),\n                    SizedBox(width: 46, child: Text(l10n.enable, textAlign: TextAlign.center)),\n                    const VerticalDivider(),\n                    const Expanded(child: Text('URL')),\n                  ],\n                ),\n                const Divider(thickness: 0.5),\n                Column(\n                    children: List.generate(rules.length, (index) {\n                  final rule = rules[index];\n                  return InkWell(\n                      highlightColor: Colors.transparent,\n                      splashColor: Colors.transparent,\n                      hoverColor: primaryColor.withOpacity(0.3),\n                      onLongPress: () => _showRuleActions(manager, index),\n                      onTap: () {\n                        if (selectionMode) {\n                          setState(() {\n                            if (!selected.add(index)) {\n                              selected.remove(index);\n                            }\n                          });\n                          return;\n                        }\n                        _editRule(manager, index);\n                      },\n                      child: Container(\n                          color: selected.contains(index)\n                              ? primaryColor.withOpacity(0.8)\n                              : index.isEven\n                                  ? Colors.grey.withOpacity(0.1)\n                                  : null,\n                          height: 45,\n                          padding: const EdgeInsets.all(5),\n                          child: Row(children: [\n                            SizedBox(\n                                width: 70,\n                                child: Text(rule.name.isEmpty ? '-' : rule.name,\n                                    overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13))),\n                            SizedBox(\n                                width: 35,\n                                child: SwitchWidget(\n                                    scale: 0.65,\n                                    value: rule.enabled,\n                                    onChanged: (val) {\n                                      rule.enabled = val;\n                                      changed = true;\n                                      setState(() {});\n                                      _refreshConfig();\n                                    })),\n                            const SizedBox(width: 20),\n                            Expanded(\n                                child: Text(rule.urlPattern.isEmpty ? l10n.emptyMatchAll : rule.urlPattern,\n                                    overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13))),\n                          ])));\n                }))\n              ])),\n      ),\n    );\n  }\n\n  Stack _buildSelectionFooter() {\n    final l10n = localizations;\n    return Stack(children: [\n      Container(\n          height: 50,\n          width: double.infinity,\n          margin: const EdgeInsets.only(top: 10),\n          decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2)))),\n      Positioned(\n          top: 0,\n          left: 0,\n          right: 0,\n          child: Center(\n              child: TextButton(\n                  onPressed: () {},\n                  child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [\n                    TextButton.icon(\n                        onPressed: selected.isEmpty\n                            ? null\n                            : () async {\n                                // export selected only\n                                final m = await RequestCryptoManager.instance;\n                                await _export(m, indexes: selected.toList());\n                                setState(() {\n                                  selected.clear();\n                                  selectionMode = false;\n                                });\n                              },\n                        icon: const Icon(Icons.share, size: 18),\n                        label: Text(l10n.export, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: selected.isEmpty ? null : () => _removeSelected(),\n                        icon: const Icon(Icons.delete, size: 18),\n                        label: Text(l10n.delete, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () {\n                          setState(() {\n                            selectionMode = false;\n                            selected.clear();\n                          });\n                        },\n                        icon: const Icon(Icons.cancel, size: 18),\n                        label: Text(l10n.cancel, style: const TextStyle(fontSize: 14))),\n                  ]))))\n    ]);\n  }\n\n  Future<void> _addRule(RequestCryptoManager manager) async {\n    Navigator.of(context).push(MaterialPageRoute(builder: (_) => const MobileCryptoRuleEditPage())).then((value) {\n      if (value != null && mounted) {\n        setState(() {});\n        _refreshConfig(force: true);\n      }\n    });\n  }\n\n  Future<void> _editRule(RequestCryptoManager manager, int index) async {\n    final rule = manager.rules[index];\n    Navigator.of(context).push(MaterialPageRoute(builder: (_) => MobileCryptoRuleEditPage(rule: rule))).then((value) {\n      if (value != null && mounted) {\n        setState(() {});\n        _refreshConfig(force: true);\n      }\n    });\n  }\n\n  void _showRuleActions(RequestCryptoManager manager, int index) {\n    final l10n = localizations;\n    setState(() {\n      selected.add(index);\n    });\n    showModalBottomSheet(\n        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),\n        context: context,\n        enableDrag: true,\n        builder: (ctx) {\n          return Wrap(children: [\n            BottomSheetItem(\n                text: l10n.multiple,\n                onPressed: () {\n                  setState(() => selectionMode = true);\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: l10n.edit,\n                onPressed: () {\n                  _editRule(manager, index);\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(text: l10n.export, onPressed: () => _export(manager, indexes: [index])),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: manager.rules[index].enabled ? l10n.disabled : l10n.enable,\n                onPressed: () {\n                  manager.rules[index].enabled = !manager.rules[index].enabled;\n                  changed = true;\n                  setState(() {});\n                  _refreshConfig();\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: l10n.delete,\n                onPressed: () {\n                  _removeRule(manager, index);\n                }),\n            Container(color: Theme.of(ctx).hoverColor, height: 8),\n            TextButton(\n                child: Container(\n                    height: 45,\n                    width: double.infinity,\n                    padding: const EdgeInsets.only(top: 10),\n                    child: Text(l10n.cancel, textAlign: TextAlign.center)),\n                onPressed: () {\n                  Navigator.of(ctx).pop();\n                }),\n          ]);\n        }).then((value) {\n      if (selectionMode) {\n        return;\n      }\n      setState(() {\n        selected.remove(index);\n      });\n    });\n  }\n\n  Future<void> _removeRule(RequestCryptoManager manager, int index) async {\n    await manager.removeRule(index);\n    if (!mounted) return;\n    changed = true;\n    setState(() {});\n    _refreshConfig(force: true);\n  }\n\n  Future<void> _removeSelected() async {\n    final l10n = localizations;\n    if (selected.isEmpty) return;\n    showConfirmDialog(context, content: l10n.confirmContent, onConfirm: () async {\n      final manager = await RequestCryptoManager.instance;\n      final indexes = selected.toList()..sort((a, b) => b.compareTo(a));\n      for (final idx in indexes) {\n        await manager.removeRule(idx);\n      }\n      if (!mounted) return;\n      changed = true;\n      setState(() {\n        selectionMode = false;\n        selected.clear();\n      });\n      _refreshConfig(force: true);\n      if (mounted) FlutterToastr.show(l10n.deleteSuccess, context);\n    });\n  }\n\n  Future<void> _import(RequestCryptoManager manager) async {\n    try {\n      FilePickerResult? result =\n          await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']);\n      final path = result?.files.single.path;\n      if (path == null) return;\n      final content = await File(path).readAsString();\n      final List list = jsonDecode(content);\n      for (final item in list) {\n        await manager.addRule(CryptoRule.fromJson(Map<String, dynamic>.from(item)));\n      }\n      if (!mounted) return;\n      changed = true;\n      setState(() {});\n      _refreshConfig(force: true);\n      FlutterToastr.show(localizations.importSuccess, context);\n    } catch (e) {\n      logger.e('导入失败', error: e);\n      if (mounted) FlutterToastr.show('${localizations.importFailed} $e', context);\n    }\n  }\n\n  Future<void> _export(RequestCryptoManager manager, {List<int>? indexes}) async {\n    try {\n      if (manager.rules.isEmpty) return;\n      final keys = (indexes == null || indexes.isEmpty)\n          ? List<int>.generate(manager.rules.length, (i) => i)\n          : (indexes.toList()..sort());\n      final data = keys.map((i) => manager.rules[i].toJson()).toList();\n      var bytes = utf8.encode(jsonEncode(data));\n      final path = await FilePicker.platform.saveFile(fileName: 'request_crypto.json', bytes: bytes);\n      if (path == null) return;\n      if (mounted) FlutterToastr.show(localizations.exportSuccess, context);\n    } catch (e) {\n      logger.e('导出失败', error: e);\n      if (mounted) FlutterToastr.show('Export failed: $e', context);\n    }\n  }\n}\n\n/// Mobile editor page for a single crypto rule.\n///\n/// This mirrors the mobile rewrite editor pattern: push to a page, edit, and save.\nclass MobileCryptoRuleEditPage extends StatefulWidget {\n  final CryptoRule? rule;\n\n  const MobileCryptoRuleEditPage({super.key, this.rule});\n\n  @override\n  State<MobileCryptoRuleEditPage> createState() => _MobileCryptoRuleEditPageState();\n}\n\nclass _MobileCryptoRuleEditPageState extends State<MobileCryptoRuleEditPage> {\n  AppLocalizations get l10n => AppLocalizations.of(context)!;\n\n  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();\n\n  late CryptoRule _rule;\n\n  late TextEditingController nameController;\n  late TextEditingController patternController;\n  late TextEditingController fieldController;\n\n  // key + iv\n  late TextEditingController keyController;\n  late TextEditingController ivController;\n\n  bool enabled = true;\n  String mode = 'CBC';\n  String padding = 'PKCS7';\n  int length = 256;\n\n  // formats & sources\n  String keyFormat = 'text'; // text | base64\n  String ivSource = 'manual'; // manual | prefix\n  int ivPrefixLength = 16;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _rule = (widget.rule ?? CryptoRule.newRule());\n\n    nameController = TextEditingController(text: _rule.name);\n    patternController = TextEditingController(text: _rule.urlPattern);\n    fieldController = TextEditingController(text: _rule.field ?? '');\n\n    enabled = _rule.enabled;\n    mode = _rule.config.mode;\n    padding = _rule.config.padding;\n    length = _rule.config.keyLength;\n\n    // key format handling (only text/base64)\n    final storedKey = _rule.config.key.trim();\n    if (storedKey.startsWith('base64:')) {\n      keyFormat = 'base64';\n      keyController = TextEditingController(text: storedKey.substring(7));\n    } else {\n      keyFormat = 'text';\n      keyController = TextEditingController(text: storedKey);\n    }\n\n    // iv source and value\n    ivSource = _rule.config.ivSource;\n    ivPrefixLength = _rule.config.ivPrefixLength;\n\n    final storedIv = _rule.config.iv.trim();\n    if (storedIv.startsWith('base64:')) {\n      ivController = TextEditingController(text: storedIv.substring(7));\n    } else {\n      ivController = TextEditingController(text: storedIv);\n    }\n  }\n\n  @override\n  void dispose() {\n    nameController.dispose();\n    patternController.dispose();\n    fieldController.dispose();\n    keyController.dispose();\n    ivController.dispose();\n    super.dispose();\n  }\n\n  InputDecoration _decorate(String label, {String? hint}) {\n    return InputDecoration(\n      labelText: label,\n      hintText: hint,\n      hintStyle: TextStyle(color: Colors.grey.withOpacity(0.8)),\n      isDense: true,\n      border: const OutlineInputBorder(),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isCN = Localizations.localeOf(context).languageCode == 'zh';\n\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(widget.rule == null ? l10n.newBuilt : l10n.edit,\n            style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n        actions: [\n          TextButton(\n            onPressed: _save,\n            child: Text(l10n.save),\n          ),\n          const SizedBox(width: 6),\n        ],\n      ),\n      body: Form(\n        key: _formKey,\n        child: ListView(\n          padding: const EdgeInsets.all(12),\n          children: [\n            Card(\n              color: Theme.of(context).colorScheme.surfaceContainerLow.withAlpha((0.5 * 255).round()),\n              elevation: 0,\n              shape: RoundedRectangleBorder(\n                side: BorderSide(color: Theme.of(context).dividerColor.withAlpha((0.2 * 255).round())),\n                borderRadius: BorderRadius.circular(8),\n              ),\n              child: Padding(\n                padding: const EdgeInsets.all(12),\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(l10n.match, style: Theme.of(context).textTheme.titleSmall),\n                    const SizedBox(height: 10),\n                    TextFormField(\n                      controller: nameController,\n                      decoration: _decorate(l10n.name),\n                    ),\n                    const SizedBox(height: 10),\n                    TextFormField(\n                      controller: patternController,\n                      decoration: _decorate('URL', hint: 'https://www.example.com/api/*'),\n                      validator: (val) => (val == null || val.trim().isEmpty) ? l10n.cannotBeEmpty : null,\n                    ),\n                    const SizedBox(height: 10),\n                    TextFormField(\n                      controller: fieldController,\n                      decoration: _decorate(l10n.cryptoRuleField, hint: isCN ? '为空=整个 body' : 'empty = whole body'),\n                    ),\n                    const SizedBox(height: 6),\n                    SwitchListTile(\n                      dense: true,\n                      contentPadding: EdgeInsets.zero,\n                      title: Text(l10n.enable),\n                      value: enabled,\n                      onChanged: (v) => setState(() => enabled = v),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n            const SizedBox(height: 12),\n            Card(\n              color: Theme.of(context).colorScheme.surfaceContainerLow.withAlpha((0.5 * 255).round()),\n              elevation: 0,\n              shape: RoundedRectangleBorder(\n                side: BorderSide(color: Theme.of(context).dividerColor.withAlpha((0.2 * 255).round())),\n                borderRadius: BorderRadius.circular(8),\n              ),\n              child: Padding(\n                padding: const EdgeInsets.all(12),\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text('AES', style: Theme.of(context).textTheme.titleSmall),\n                    const SizedBox(height: 10),\n                    Wrap(\n                      spacing: 12,\n                      runSpacing: 10,\n                      crossAxisAlignment: WrapCrossAlignment.center,\n                      children: [\n                        _kvDropdown(\n                          label: 'Mode',\n                          child: DropdownButton<String>(\n                            value: mode,\n                            items: const [\n                              DropdownMenuItem(value: 'ECB', child: Text('ECB')),\n                              DropdownMenuItem(value: 'CBC', child: Text('CBC')),\n                            ],\n                            onChanged: (v) => setState(() => mode = v ?? 'CBC'),\n                          ),\n                        ),\n                        _kvDropdown(\n                          label: 'Padding',\n                          child: DropdownButton<String>(\n                            value: padding,\n                            items: const [\n                              DropdownMenuItem(value: 'PKCS7', child: Text('PKCS7')),\n                              DropdownMenuItem(value: 'ZeroPadding', child: Text('ZeroPadding')),\n                            ],\n                            onChanged: (v) => setState(() => padding = v ?? 'PKCS7'),\n                          ),\n                        ),\n                        _kvDropdown(\n                          label: 'Key Length',\n                          child: DropdownButton<int>(\n                            value: length,\n                            items: const [\n                              DropdownMenuItem(value: 128, child: Text('128')),\n                              DropdownMenuItem(value: 192, child: Text('192')),\n                              DropdownMenuItem(value: 256, child: Text('256')),\n                            ],\n                            onChanged: (v) => setState(() => length = v ?? 256),\n                          ),\n                        ),\n                      ],\n                    ),\n                    const SizedBox(height: 10),\n                    Row(\n                      children: [\n                        _chipDropdown(\n                          value: keyFormat,\n                          items: const [\n                            DropdownMenuItem(value: 'text', child: Text('text')),\n                            DropdownMenuItem(value: 'base64', child: Text('base64')),\n                          ],\n                          onChanged: (v) => setState(() => keyFormat = v ?? 'text'),\n                        ),\n                        const SizedBox(width: 10),\n                        Expanded(\n                          child: TextFormField(\n                            controller: keyController,\n                            decoration: _decorate('Key'),\n                            validator: (val) => (val == null || val.trim().isEmpty) ? l10n.cannotBeEmpty : null,\n                          ),\n                        ),\n                      ],\n                    ),\n                    const SizedBox(height: 10),\n                    if (mode == 'CBC') ...[\n                      Row(\n                        children: [\n                          _chipDropdown(\n                            value: ivSource,\n                            items: [\n                              DropdownMenuItem(value: 'manual', child: Text(l10n.manual)),\n                              DropdownMenuItem(value: 'prefix', child: Text(l10n.cryptoIvPrefixLabel)),\n                            ],\n                            onChanged: (v) => setState(() => ivSource = v ?? 'manual'),\n                          ),\n                          const SizedBox(width: 10),\n                          Expanded(\n                            child: ivSource == 'manual'\n                                ? TextFormField(\n                                    controller: ivController,\n                                    decoration: _decorate('IV'),\n                                    validator: (val) => (ivSource == 'manual' && (val == null || val.trim().isEmpty))\n                                        ? l10n.cannotBeEmpty\n                                        : null,\n                                  )\n                                : _ivPrefixLengthEditor(),\n                          ),\n                        ],\n                      ),\n                      if (ivSource == 'prefix')\n                        Padding(\n                          padding: const EdgeInsets.only(top: 6),\n                          child: Text(\n                            l10n.cryptoIvPrefixTooltip,\n                            style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey),\n                          ),\n                        ),\n                    ],\n                  ],\n                ),\n              ),\n            ),\n            const SizedBox(height: 24),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _kvDropdown({required String label, required Widget child}) {\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Text(label),\n        const SizedBox(width: 8),\n        Container(\n          height: 40,\n          padding: const EdgeInsets.symmetric(horizontal: 10),\n          decoration: BoxDecoration(\n            border: Border.all(color: Theme.of(context).dividerColor.withValues(alpha: 0.25)),\n            borderRadius: BorderRadius.circular(8),\n          ),\n          child: DropdownButtonHideUnderline(child: child),\n        ),\n      ],\n    );\n  }\n\n  Widget _chipDropdown<T>({\n    required T value,\n    required List<DropdownMenuItem<T>> items,\n    required ValueChanged<T?> onChanged,\n  }) {\n    return Container(\n      height: 40,\n      constraints: const BoxConstraints(minWidth: 95),\n      padding: const EdgeInsets.symmetric(horizontal: 6),\n      decoration: BoxDecoration(\n          border: Border.all(color: Theme.of(context).dividerColor.withValues(alpha: 0.25)),\n          borderRadius: BorderRadius.circular(6)),\n      child: DropdownButtonHideUnderline(\n        child: DropdownButton<T>(\n          value: value,\n          items: items,\n          onChanged: onChanged,\n        ),\n      ),\n    );\n  }\n\n  Widget _ivPrefixLengthEditor() {\n    return Container(\n      height: 40,\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      decoration: BoxDecoration(\n        border: Border.all(color: Theme.of(context).dividerColor.withValues(alpha: 0.25)),\n        borderRadius: BorderRadius.circular(8),\n      ),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          IconButton(\n            padding: EdgeInsets.zero,\n            constraints: const BoxConstraints.tightFor(width: 28, height: 28),\n            icon: const Icon(Icons.remove, size: 16),\n            onPressed: () => setState(() => ivPrefixLength = math.max(1, ivPrefixLength - 1)),\n          ),\n          Text(ivPrefixLength.toString()),\n          IconButton(\n            padding: EdgeInsets.zero,\n            constraints: const BoxConstraints.tightFor(width: 28, height: 28),\n            icon: const Icon(Icons.add, size: 16),\n            onPressed: () => setState(() => ivPrefixLength = math.min(1024, ivPrefixLength + 1)),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future<void> _save() async {\n    if (!(_formKey.currentState?.validate() ?? false)) {\n      FlutterToastr.show(l10n.cannotBeEmpty, context, position: FlutterToastr.center);\n      return;\n    }\n\n    var outKey = keyController.text.trim();\n    if (!outKey.startsWith('base64:') && keyFormat == 'base64') {\n      outKey = 'base64:$outKey';\n    }\n\n    String outIv = '';\n    if (ivSource == 'manual') {\n      outIv = ivController.text.trim();\n      if (!outIv.startsWith('base64:') && keyFormat == 'base64') {\n        outIv = 'base64:$outIv';\n      }\n    }\n\n    final updated = _rule.copyWith(\n      name: nameController.text.trim(),\n      urlPattern: patternController.text.trim(),\n      field: fieldController.text.trim(),\n      enabled: enabled,\n      config: CryptoKeyConfig(\n        key: outKey,\n        iv: outIv,\n        ivSource: ivSource,\n        ivPrefixLength: ivPrefixLength,\n        mode: mode,\n        padding: padding,\n        keyLength: length,\n      ),\n    );\n\n    final manager = await RequestCryptoManager.instance;\n    final idx = manager.rules.indexOf(_rule);\n\n    if (idx >= 0) {\n      await manager.updateRule(idx, updated);\n    } else {\n      await manager.addRule(updated);\n    }\n    await manager.flushConfig();\n\n    if (!mounted) return;\n    FlutterToastr.show(l10n.saveSuccess, context);\n    Navigator.of(context).pop(updated);\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/request_map/map_local.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:get/get.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_map_manager.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/ui/component/state_component.dart';\n\nimport '../../../component/utils.dart';\n\n/// 重写替换\n/// @author wanghongen\n/// 2023/10/8\nclass MobileMapLocal extends StatefulWidget {\n  final RequestMapItem? item;\n  final ScrollController? scrollController;\n\n  const MobileMapLocal({super.key, this.item, this.scrollController});\n\n  @override\n  State<MobileMapLocal> createState() => MobileMapLocaleState();\n}\n\nclass MobileMapLocaleState extends State<MobileMapLocal> {\n  final _headerKey = GlobalKey<HeadersState>();\n  final bodyTextController = TextEditingController();\n\n  RxString bodyType = RxString(ReplaceBodyType.text.name);\n  Rxn<String> bodyFile = Rxn<String>();\n  TextEditingController statusCodeController = TextEditingController(text: '200');\n\n  late ScrollController? bodyScrollController;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  initState() {\n    super.initState();\n    initItem(widget.item);\n    bodyScrollController = trackingScroll(widget.scrollController);\n  }\n\n  @override\n  dispose() {\n    bodyTextController.dispose();\n    statusCodeController.dispose();\n    bodyScrollController?.dispose();\n    super.dispose();\n  }\n\n  ///初始化重写项\n  void initItem(RequestMapItem? item) {\n    if (item == null) {\n      return;\n    }\n    statusCodeController.text = item.statusCode?.toString() ?? '200';\n    bodyTextController.text = item.body ?? '';\n    bodyType.value = item.bodyType ?? ReplaceBodyType.text.name;\n    if (item.bodyType == ReplaceBodyType.file.name) {\n      bodyFile.value = item.bodyFile;\n    }\n  }\n\n  RequestMapItem getRequestMapItem() {\n    RequestMapItem item = widget.item ?? RequestMapItem();\n    var headers = _headerKey.currentState?.getHeaders() ?? widget.item?.headers;\n    item.statusCode = int.tryParse(statusCodeController.text) ?? 200;\n    item.headers = headers;\n    item.body = bodyTextController.text;\n    item.bodyType = bodyType.value;\n    if (item.bodyType == ReplaceBodyType.file.name) {\n      item.bodyFile = bodyFile.value;\n    } else {\n      item.bodyFile = null;\n    }\n    return item;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    List<String> tabs = [localizations.statusCode, localizations.responseHeader, localizations.responseBody];\n\n    return DefaultTabController(\n        length: tabs.length,\n        initialIndex: tabs.length - 1,\n        child: Scaffold(\n          appBar: tabBar(tabs),\n          body: TabBarView(children: [\n            KeepAliveWrapper(child: statusCodeEdit()),\n            KeepAliveWrapper(child: headers()),\n            KeepAliveWrapper(child: body())\n          ]),\n        ));\n  }\n\n  //tabBar\n  TabBar tabBar(List<String> tabs) {\n    return TabBar(\n        tabs: tabs\n            .map((label) => Tab(\n                  height: 38,\n                  child: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n                ))\n            .toList());\n  }\n\n  //body\n  Widget body() {\n    bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en');\n\n    return Obx(() => ListView(physics: const ClampingScrollPhysics(), children: [\n          Row(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [\n            const SizedBox(width: 5),\n            Text(\"${localizations.type}: \"),\n            SizedBox(\n                width: 90,\n                child: DropdownButtonFormField<String>(\n                    value: bodyType.value,\n                    focusColor: Colors.transparent,\n                    itemHeight: 48,\n                    decoration: const InputDecoration(\n                        contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none),\n                    items: ReplaceBodyType.values\n                        .map((e) => DropdownMenuItem(\n                            value: e.name,\n                            child: Text(isEN ? e.name.toUpperCase() : e.label,\n                                style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500))))\n                        .toList(),\n                    onChanged: (val) => bodyType.value = val ?? ReplaceBodyType.text.name)),\n          ]),\n          const SizedBox(height: 10),\n          if (bodyType.value == ReplaceBodyType.file.name)\n            fileBodyEdit()\n          else\n            TextFormField(\n                controller: bodyTextController,\n                scrollPhysics: const BouncingScrollPhysics(),\n                scrollController: bodyScrollController,\n                style: const TextStyle(fontSize: 14),\n                minLines: 18,\n                maxLines: 20,\n                decoration: decoration(localizations.replaceBodyWith,\n                    hintText: '${localizations.example} {\"code\":\"200\",\"data\":{}}')),\n        ]));\n  }\n\n  Widget fileBodyEdit() {\n    //选择文件  删除\n    return Obx(() => Row(crossAxisAlignment: CrossAxisAlignment.start, children: [\n          Expanded(\n              child: bodyFile.value == null\n                  ? Container(height: 50)\n                  : Container(\n                      padding: const EdgeInsets.all(5),\n                      foregroundDecoration:\n                          BoxDecoration(border: Border.all(color: Theme.of(context).colorScheme.primary, width: 1)),\n                      child: Text(bodyFile.value ?? ''))),\n          const SizedBox(width: 10),\n          FilledButton(\n              onPressed: () async {\n                FilePickerResult? result = await FilePicker.platform.pickFiles();\n                String? path = result?.files.single.path;\n\n                if (path == null) {\n                  return;\n                }\n                bodyFile.value = path;\n              },\n              child: Text(localizations.selectFile, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500))),\n          const SizedBox(width: 10),\n          FilledButton(\n              onPressed: () {\n                setState(() {\n                  bodyFile.value = null;\n                });\n              },\n              child: Text(localizations.delete, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500))),\n        ]));\n  }\n\n  //headers\n  Widget headers() {\n    return Headers(headers: widget.item?.headers, key: _headerKey);\n  }\n\n  Widget textField(String label, dynamic value, String hint, {ValueChanged<String>? onChanged}) {\n    return Row(children: [\n      SizedBox(width: 80, child: Text(label)),\n      Expanded(\n          child: TextFormField(\n        initialValue: value,\n        onChanged: onChanged,\n        decoration: InputDecoration(\n            hintText: hint,\n            hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n            contentPadding: const EdgeInsets.all(10),\n            errorStyle: const TextStyle(height: 0, fontSize: 0),\n            focusedBorder: focusedBorder(),\n            isDense: true,\n            border: const OutlineInputBorder()),\n      ))\n    ]);\n  }\n\n  Widget statusCodeEdit() {\n    return Container(\n        padding: const EdgeInsets.all(10),\n        child: Column(children: [\n          Row(crossAxisAlignment: CrossAxisAlignment.center, children: [\n            Text(localizations.statusCode),\n            const SizedBox(width: 10),\n            SizedBox(\n                width: 100,\n                child: TextFormField(\n                  controller: statusCodeController,\n                  style: const TextStyle(fontSize: 14),\n                  inputFormatters: [FilteringTextInputFormatter.digitsOnly],\n                  decoration: InputDecoration(\n                      contentPadding: const EdgeInsets.all(10),\n                      focusedBorder: focusedBorder(),\n                      isDense: true,\n                      border: const OutlineInputBorder()),\n                )),\n            const SizedBox(width: 10),\n          ])\n        ]));\n  }\n\n  InputDecoration decoration(String label, {String? hintText}) {\n    Color color = Theme.of(context).colorScheme.primary;\n    // Color color = Colors.blueAccent;\n    return InputDecoration(\n        floatingLabelBehavior: FloatingLabelBehavior.always,\n        labelText: label,\n        hintStyle: TextStyle(color: Colors.grey.shade500),\n        hintText: hintText,\n        isDense: true,\n        border: OutlineInputBorder(borderSide: BorderSide(width: 0.8, color: color)),\n        enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.5, color: color)),\n        focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color)));\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n\n///请求头\nclass Headers extends StatefulWidget {\n  final Map<String, String>? headers;\n\n  const Headers({super.key, this.headers});\n\n  @override\n  State<StatefulWidget> createState() {\n    return HeadersState();\n  }\n}\n\nclass HeadersState extends State<Headers> with AutomaticKeepAliveClientMixin {\n  final Map<TextEditingController, TextEditingController> _headers = {};\n\n  @override\n  bool get wantKeepAlive => true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.headers == null) {\n      _headers[TextEditingController()] = TextEditingController();\n      return;\n    }\n\n    setHeaders(widget.headers);\n  }\n\n  void setHeaders(Map<String, String>? headers) {\n    _clear();\n    headers?.forEach((name, value) {\n      _headers[TextEditingController(text: name)] = TextEditingController(text: value);\n    });\n    _headers[TextEditingController()] = TextEditingController();\n  }\n\n  ///获取所有请求头\n  Map<String, String> getHeaders() {\n    var headers = <String, String>{};\n    _headers.forEach((name, value) {\n      if (name.text.isEmpty) {\n        return;\n      }\n      headers[name.text] = value.text;\n    });\n    return headers;\n  }\n\n  @override\n  dispose() {\n    _clear();\n    super.dispose();\n  }\n\n  void _clear() {\n    _headers.forEach((key, value) {\n      key.dispose();\n      value.dispose();\n    });\n    _headers.clear();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    var list = _buildRows();\n\n    return Column(children: [\n      Expanded(\n          child: Padding(\n              padding: const EdgeInsets.only(top: 10, bottom: 10),\n              child: ListView.separated(\n                  shrinkWrap: true,\n                  separatorBuilder: (context, index) =>\n                      index == list.length ? const SizedBox() : const Divider(thickness: 0.2),\n                  itemBuilder: (context, index) => list[index],\n                  itemCount: list.length))),\n      TextButton(\n        child: Text(\"${localizations.add}Header\", textAlign: TextAlign.center),\n        onPressed: () {\n          setState(() {\n            _headers[TextEditingController()] = TextEditingController();\n          });\n        },\n      ),\n    ]);\n  }\n\n  List<Widget> _buildRows() {\n    List<Widget> list = [];\n\n    _headers.forEach((key, val) {\n      list.add(_row(\n          _cell(key, isKey: true),\n          _cell(val),\n          Padding(\n              padding: const EdgeInsets.only(right: 15),\n              child: InkWell(\n                  onTap: () {\n                    setState(() {\n                      _headers.remove(key);\n                    });\n                  },\n                  child: const Icon(Icons.remove_circle_outline, size: 16)))));\n    });\n\n    return list;\n  }\n\n  Widget _cell(TextEditingController val, {bool isKey = false}) {\n    return Container(\n        padding: const EdgeInsets.only(right: 5),\n        child: TextFormField(\n            style: TextStyle(fontSize: 12, fontWeight: isKey ? FontWeight.w500 : null),\n            controller: val,\n            minLines: 1,\n            maxLines: 3,\n            decoration: InputDecoration(\n                isDense: true,\n                border: const OutlineInputBorder(),\n                enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 0.5, color: Colors.grey)),\n                hintStyle: TextStyle(fontSize: 12, color: Colors.grey),\n                hintText: isKey ? \"Key\" : \"Value\")));\n  }\n\n  Widget _row(Widget key, Widget val, Widget? op) {\n    return Row(children: [\n      Expanded(flex: 4, child: key),\n      const Text(\": \", style: TextStyle(color: Colors.deepOrangeAccent)),\n      Expanded(flex: 6, child: val),\n      op ?? const SizedBox()\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/request_map/map_scipt.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_code_editor/flutter_code_editor.dart';\nimport 'package:flutter_highlight/themes/monokai-sublime.dart';\nimport 'package:highlight/languages/javascript.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\n\nclass MobileMapScript extends StatefulWidget {\n  final String? script;\n\n  const MobileMapScript({super.key, this.script});\n\n  @override\n  State<MobileMapScript> createState() => MobileMapScriptState();\n}\n\nclass MobileMapScriptState extends State<MobileMapScript> {\n  static String template = \"\"\"\nasync function onRequest(context, request) {\n  console.log(request.url);\n  //use fetch API request\n  // var result = await fetch('https://www.baidu.com/');\n  var response = {\n    statusCode: 200,\n    body: 'Hello, world!',\n    headers: {\n      'Content-Type': 'text/plain',\n      'X-My-Header': 'My-Value'\n    }\n  };\n  return response;\n}\n  \"\"\";\n  late CodeController script;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  String getScriptCode() {\n    return script.text;\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    script = CodeController(language: javascript, text: widget.script ?? template);\n  }\n\n  @override\n  void dispose() {\n    script.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n        // height: double.infinity,\n        child: CodeTheme(\n            data: CodeThemeData(styles: monokaiSublimeTheme),\n            child: SingleChildScrollView(\n              child: CodeField(\n                  textStyle: const TextStyle(fontSize: 13),\n                  enableSuggestions: true,\n                  gutterStyle: const GutterStyle(width: 50, margin: 0),\n                  onTapOutside: (event) => FocusScope.of(context).unfocus(),\n                  controller: script),\n            )));\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/request_map.dart",
    "content": "import 'dart:convert';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/request_map_manager.dart';\nimport 'package:proxypin/ui/component/app_dialog.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/mobile/setting/request_map/map_local.dart';\nimport 'package:proxypin/ui/mobile/setting/request_map/map_scipt.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:share_plus/share_plus.dart';\n\nimport '../../../../network/util/logger.dart';\nimport '../../../utils/platform.dart';\n\nbool _refresh = false;\n\n/// 刷新配置\nvoid _refreshConfig({bool force = false}) {\n  if (_refresh && !force) {\n    return;\n  }\n  _refresh = true;\n  Future.delayed(const Duration(milliseconds: 1500), () async {\n    _refresh = false;\n    await RequestMapManager.instance.then((manager) => manager.flushConfig());\n  });\n}\n\nclass MobileRequestMapPage extends StatefulWidget {\n  const MobileRequestMapPage({super.key});\n\n  @override\n  State<StatefulWidget> createState() => _RequestMapPageState();\n}\n\nclass _RequestMapPageState extends State<MobileRequestMapPage> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n            title: Text(localizations.requestMap, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n            toolbarHeight: 36,\n            centerTitle: true),\n        body: Padding(\n            padding: const EdgeInsets.all(10),\n            child: futureWidget(\n                RequestMapManager.instance,\n                loading: true,\n                (data) => Column(children: [\n                      Row(children: [\n                        Expanded(\n                            child: ListTile(\n                                title: Text(\"${localizations.enable} ${localizations.requestMap}\"),\n                                subtitle: Text(localizations.requestMapDescribe, style: const TextStyle(fontSize: 12)),\n                                trailing: SwitchWidget(\n                                    value: data.enabled,\n                                    scale: 0.8,\n                                    onChanged: (value) {\n                                      data.enabled = value;\n                                      _refreshConfig();\n                                    }))),\n                      ]),\n                      Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n                        const SizedBox(width: 10),\n                        TextButton.icon(\n                            icon: const Icon(Icons.add, size: 18), onPressed: showEdit, label: Text(localizations.add)),\n                        const SizedBox(width: 10),\n                        TextButton.icon(\n                          icon: const Icon(Icons.input_rounded, size: 18),\n                          onPressed: import,\n                          label: Text(localizations.import),\n                        ),\n                        const SizedBox(width: 10),\n                      ]),\n                      const SizedBox(height: 10),\n                      Expanded(child: RequestMapList(list: data.rules)),\n                    ]))));\n  }\n\n  //导入js\n  Future<void> import() async {\n    FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any);\n    if (result == null || result.files.isEmpty) {\n      return;\n    }\n    var file = result.files.single.xFile;\n\n    try {\n      List json = jsonDecode(utf8.decode(await file.readAsBytes()));\n\n      var manager = (await RequestMapManager.instance);\n      for (var item in json) {\n        var mapRule = RequestMapRule.fromJson(item);\n        var requestMapItem = RequestMapItem.fromJson(item['item']);\n        await manager.addRule(mapRule, requestMapItem);\n      }\n\n      if (mounted) {\n        CustomToast.success(localizations.importSuccess).show(context);\n      }\n      setState(() {});\n    } catch (e, t) {\n      logger.e('[RequestMap] import fail $file', error: e, stackTrace: t);\n      if (mounted) {\n        CustomToast.error(\"${localizations.importFailed} $e\").show(context);\n      }\n    }\n  }\n\n  /// 添加脚本\n  Future<void> showEdit() async {\n    Navigator.push(context, MaterialPageRoute(builder: (_) => const MobileRequestMapEdit())).then((value) {\n      if (value != null) {\n        setState(() {});\n      }\n    });\n  }\n}\n\n/// 脚本列表\nclass RequestMapList extends StatefulWidget {\n  final List<RequestMapRule> list;\n\n  const RequestMapList({super.key, required this.list});\n\n  @override\n  State<RequestMapList> createState() => _RequestMapListState();\n}\n\nclass _RequestMapListState extends State<RequestMapList> {\n  Set<int> selected = {};\n  bool multiple = false;\n  bool changed = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void dispose() {\n    if (changed) {\n      _refreshConfig(force: true);\n    }\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        persistentFooterButtons: multiple ? [globalMenu()] : null,\n        body: Container(\n            padding: const EdgeInsets.only(top: 10),\n            decoration: BoxDecoration(\n              border: Border.all(color: Colors.grey.withOpacity(0.2)),\n            ),\n            child: Scrollbar(\n                child: ListView(children: [\n              Row(\n                mainAxisAlignment: MainAxisAlignment.start,\n                children: [\n                  Container(width: 130, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)),\n                  SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)),\n                  const VerticalDivider(),\n                  const Expanded(child: Text(\"URL\")),\n                  SizedBox(width: 100, child: Text(localizations.action, textAlign: TextAlign.center)),\n                ],\n              ),\n              const Divider(thickness: 0.5),\n              Column(children: rows(widget.list))\n            ]))));\n  }\n\n  List<Widget> rows(List<RequestMapRule> list) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n    bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en');\n\n    return List.generate(list.length, (index) {\n      return InkWell(\n          highlightColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          hoverColor: primaryColor.withOpacity(0.3),\n          onLongPress: () => showMenus(index),\n          onTap: () async {\n            if (multiple) {\n              setState(() {\n                if (!selected.add(index)) {\n                  selected.remove(index);\n                }\n              });\n              return;\n            }\n            showEdit(index);\n          },\n          child: Container(\n              color: selected.contains(index)\n                  ? primaryColor.withOpacity(0.6)\n                  : index.isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 30,\n              padding: const EdgeInsets.all(5),\n              child: Row(\n                children: [\n                  SizedBox(width: 60, child: Text(list[index].name ?? '', style: const TextStyle(fontSize: 13))),\n                  SizedBox(\n                      width: 35,\n                      child: Transform.scale(\n                          scale: 0.6,\n                          child: SwitchWidget(\n                              value: list[index].enabled,\n                              onChanged: (val) {\n                                list[index].enabled = val;\n                                _refreshConfig();\n                              }))),\n                  const SizedBox(width: 20),\n                  Expanded(\n                      child:\n                          Text(list[index].url, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13))),\n                  const SizedBox(width: 3),\n                  SizedBox(\n                      width: 60,\n                      child: Text(isEN ? list[index].type.name.camelCaseToSpaced() : list[index].type.label,\n                          textAlign: TextAlign.center, style: const TextStyle(fontSize: 13))),\n                ],\n              )));\n    });\n  }\n\n  Stack globalMenu() {\n    return Stack(children: [\n      Container(\n          height: 50,\n          width: double.infinity,\n          margin: const EdgeInsets.only(top: 10),\n          decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2)))),\n      Positioned(\n          top: 0,\n          left: 0,\n          right: 0,\n          child: Center(\n              child: TextButton(\n                  onPressed: () {},\n                  child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [\n                    TextButton.icon(\n                        onPressed: () {\n                          export(selected.toList());\n                          setState(() {\n                            selected.clear();\n                            multiple = false;\n                          });\n                        },\n                        icon: const Icon(Icons.share, size: 18),\n                        label: Text(localizations.export, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () => remove(selected.toList()),\n                        icon: const Icon(Icons.delete, size: 18),\n                        label: Text(localizations.delete, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () {\n                          setState(() {\n                            multiple = false;\n                            selected.clear();\n                          });\n                        },\n                        icon: const Icon(Icons.cancel, size: 18),\n                        label: Text(localizations.cancel, style: const TextStyle(fontSize: 14))),\n                  ]))))\n    ]);\n  }\n\n  //点击菜单\n  void showMenus(int index) {\n    setState(() {\n      selected.add(index);\n    });\n\n    showModalBottomSheet(\n        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),\n        context: context,\n        enableDrag: true,\n        builder: (ctx) {\n          return Wrap(alignment: WrapAlignment.center, children: [\n            BottomSheetItem(\n                text: localizations.multiple,\n                onPressed: () {\n                  setState(() => multiple = true);\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(text: localizations.edit, onPressed: () => showEdit(index)),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(text: localizations.export, onPressed: () => export([index])),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: widget.list[index].enabled ? localizations.disabled : localizations.enable,\n                onPressed: () {\n                  widget.list[index].enabled = !widget.list[index].enabled;\n                  _refreshConfig();\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: localizations.delete,\n                onPressed: () async {\n                  var manager = await RequestMapManager.instance;\n                  await manager.deleteRule(index);\n                  _refreshConfig();\n                }),\n          ]);\n        }).then((value) {\n      if (multiple) {\n        return;\n      }\n      setState(() {\n        selected.remove(index);\n      });\n    });\n  }\n\n  Future<void> showEdit([int? index]) async {\n    final item = index == null ? null : await (await RequestMapManager.instance).getMapItem(widget.list[index]);\n    if (!mounted) {\n      return;\n    }\n\n    Navigator.push(\n            context,\n            MaterialPageRoute(\n                builder: (_) => MobileRequestMapEdit(rule: index == null ? null : widget.list[index], item: item)))\n        .then((value) {\n      if (value != null) {\n        setState(() {});\n      }\n    });\n  }\n\n  //导出\n  Future<void> export(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    //文件名称\n    String fileName = 'request_map.json';\n\n    var manager = await RequestMapManager.instance;\n    List<dynamic> json = [];\n    for (var idx in indexes) {\n      var item = widget.list[idx];\n      var map = item.toJson();\n      map.remove(\"itemPath\");\n      map['item'] = (await manager.getMapItem(item))?.toJson();\n      json.add(map);\n    }\n\n    RenderBox? box;\n    if (await Platforms.isIpad() && mounted) {\n      box = context.findRenderObject() as RenderBox?;\n    }\n\n    final XFile file = XFile.fromData(utf8.encode(jsonEncode(json)), mimeType: 'config');\n    ShareParams shareParams = ShareParams(\n      files: [file],\n      fileNameOverrides: [fileName],\n      sharePositionOrigin: box?.paintBounds,\n    );\n    await SharePlus.instance.share(shareParams);\n  }\n\n  void enableStatus(bool enable) {\n    for (var idx in selected) {\n      widget.list[idx].enabled = enable;\n    }\n    setState(() {});\n    _refreshConfig();\n  }\n\n  Future<void> remove(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    showConfirmDialog(context, content: localizations.confirmContent, onConfirm: () async {\n      var manager = await RequestMapManager.instance;\n      for (var idx in indexes) {\n        await manager.deleteRule(idx);\n      }\n\n      setState(() {\n        selected.clear();\n      });\n      _refreshConfig(force: true);\n\n      if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n    });\n  }\n}\n\n///请求重写规则添加对话框\nclass MobileRequestMapEdit extends StatefulWidget {\n  final RequestMapRule? rule;\n  final RequestMapItem? item;\n  final String? url;\n  final String? title;\n\n  const MobileRequestMapEdit({super.key, this.rule, this.item, this.url, this.title});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _RequestMapEditState();\n  }\n}\n\nclass _RequestMapEditState extends State<MobileRequestMapEdit> {\n  final mapLocalKey = GlobalKey<MobileMapLocaleState>();\n  final mapScriptKey = GlobalKey<MobileMapScriptState>();\n  final ScrollController scrollController = ScrollController();\n\n  late RequestMapRule rule;\n\n  late RequestMapType mapType;\n  late TextEditingController nameInput;\n  late TextEditingController urlInput;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    rule = widget.rule ?? RequestMapRule(url: widget.url ?? '', name: widget.title, type: RequestMapType.local);\n    mapType = rule.type;\n    nameInput = TextEditingController(text: rule.name);\n    urlInput = TextEditingController(text: rule.url);\n  }\n\n  @override\n  void dispose() {\n    urlInput.dispose();\n    nameInput.dispose();\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    GlobalKey formKey = GlobalKey<FormState>();\n    bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en');\n\n    return Scaffold(\n        appBar: AppBar(\n            title: Row(children: [\n              Text(localizations.requestRewriteRule, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n            ]),\n            actions: [\n              TextButton(\n                  child: Text(localizations.save),\n                  onPressed: () async {\n                    if (!(formKey.currentState as FormState).validate()) {\n                      FlutterToastr.show(localizations.cannotBeEmpty, context, position: FlutterToastr.center);\n                      return;\n                    }\n\n                    (formKey.currentState as FormState).save();\n                    rule.name = nameInput.text;\n                    rule.url = urlInput.text;\n                    rule.type = mapType;\n                    RequestMapItem item;\n                    if (mapType == RequestMapType.local) {\n                      item = mapLocalKey.currentState!.getRequestMapItem();\n                    } else {\n                      String? scriptCode = mapScriptKey.currentState?.getScriptCode();\n                      item = widget.item ?? RequestMapItem();\n                      item.script = scriptCode;\n                    }\n\n                    var requestMapManager = await RequestMapManager.instance;\n                    var index = requestMapManager.rules.indexOf(rule);\n                    if (index >= 0) {\n                      await requestMapManager.updateRule(rule, item);\n                    } else {\n                      await requestMapManager.addRule(rule, item);\n                    }\n\n                    if (mounted) {\n                      Navigator.of(this.context).pop(rule);\n                    }\n                  })\n            ]),\n        body: Container(\n          padding: const EdgeInsets.all(15),\n          child: NestedScrollView(\n            controller: scrollController,\n            headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {\n              return <Widget>[\n                SliverToBoxAdapter(\n                    child: Form(\n                        key: formKey,\n                        child: Column(\n                            mainAxisAlignment: MainAxisAlignment.start,\n                            crossAxisAlignment: CrossAxisAlignment.start,\n                            children: <Widget>[\n                              Row(children: [\n                                SizedBox(width: 55, child: Text('${localizations.enable}:')),\n                                SwitchWidget(value: rule.enabled, onChanged: (val) => rule.enabled = val, scale: 0.8)\n                              ]),\n                              const SizedBox(height: 5),\n                              textField('${localizations.name}:', nameInput, localizations.pleaseEnter),\n                              const SizedBox(height: 5),\n                              textField('URL:', urlInput, 'https://www.example.com/api/*', required: true),\n                              const SizedBox(height: 5),\n                              Row(children: [\n                                SizedBox(width: 60, child: Text('${localizations.action}:')),\n                                SizedBox(\n                                    width: 150,\n                                    height: 33,\n                                    child: DropdownButtonFormField<RequestMapType>(\n                                      onSaved: (val) => rule.type = val!,\n                                      value: mapType,\n                                      decoration: InputDecoration(\n                                          errorStyle: const TextStyle(height: 0, fontSize: 0),\n                                          contentPadding: const EdgeInsets.only(left: 7, right: 7),\n                                          focusedBorder: focusedBorder(),\n                                          border: const OutlineInputBorder()),\n                                      items: RequestMapType.values\n                                          .map((e) => DropdownMenuItem(\n                                              value: e,\n                                              child:\n                                                  Text(isEN ? e.name : e.label, style: const TextStyle(fontSize: 13))))\n                                          .toList(),\n                                      onChanged: onChangeType,\n                                    )),\n                                const SizedBox(width: 10),\n                              ]),\n                              const SizedBox(height: 10),\n                            ])))\n              ];\n            },\n            body: mapRule(),\n          ),\n        ));\n  }\n\n  void onChangeType(RequestMapType? val) async {\n    if (mapType == val) return;\n    mapType = val!;\n    setState(() {});\n  }\n\n  Widget mapRule() {\n    if (mapType == RequestMapType.script) {\n      return MobileMapScript(key: mapScriptKey, script: widget.item?.script);\n    }\n\n    return MobileMapLocal(scrollController: scrollController, key: mapLocalKey, item: widget.item);\n  }\n\n  Widget textField(String label, TextEditingController controller, String hint,\n      {bool required = false, FormFieldSetter<String>? onSaved}) {\n    return Row(children: [\n      SizedBox(width: 60, child: Text(label)),\n      Expanded(\n          child: TextFormField(\n        controller: controller,\n        style: const TextStyle(fontSize: 14),\n        validator: (val) => val?.isNotEmpty == true || !required ? null : \"\",\n        onSaved: onSaved,\n        decoration: InputDecoration(\n            hintText: hint,\n            constraints: const BoxConstraints(minHeight: 38),\n            hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n            contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),\n            errorStyle: const TextStyle(height: 0, fontSize: 0),\n            focusedBorder: focusedBorder(),\n            isDense: true,\n            border: const OutlineInputBorder()),\n      ))\n    ]);\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/request_rewrite.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:collection';\nimport 'dart:convert';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/mobile/setting/rewrite/rewrite_update.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:share_plus/share_plus.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nimport '../../component/http_method_popup.dart';\nimport 'rewrite/rewrite_replace.dart';\n\nclass MobileRequestRewrite extends StatefulWidget {\n  final RequestRewriteManager requestRewrites;\n\n  const MobileRequestRewrite({super.key, required this.requestRewrites});\n\n  @override\n  State<MobileRequestRewrite> createState() => _MobileRequestRewriteState();\n}\n\nclass _MobileRequestRewriteState extends State<MobileRequestRewrite> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n            centerTitle: true, title: Text(localizations.requestRewriteList, style: const TextStyle(fontSize: 16))),\n        body: Container(\n            padding: const EdgeInsets.all(10),\n            child: Column(\n              children: [\n                Row(\n                  children: [\n                    Text(localizations.requestRewriteEnable),\n                    SwitchWidget(\n                        value: widget.requestRewrites.enabled,\n                        scale: 0.8,\n                        onChanged: (val) {\n                          widget.requestRewrites.enabled = val;\n                          widget.requestRewrites.flushRequestRewriteConfig();\n                        }),\n                  ],\n                ),\n                Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n                  TextButton.icon(\n                      icon: const Icon(Icons.add, size: 20), onPressed: add, label: Text(localizations.add)),\n                  const SizedBox(width: 5),\n                  TextButton.icon(\n                      icon: const Icon(Icons.input_rounded, size: 20),\n                      onPressed: import,\n                      label: Text(localizations.import)),\n                ]),\n                const SizedBox(height: 10),\n                Expanded(child: RequestRuleList(widget.requestRewrites)),\n              ],\n            )));\n  }\n\n  //导入\n  Future<void> import() async {\n    FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any);\n    if (result == null || result.files.isEmpty) {\n      return;\n    }\n    var file = result.files.single.xFile;\n\n    try {\n      List json = jsonDecode(utf8.decode(await file.readAsBytes()));\n\n      for (var item in json) {\n        var rule = RequestRewriteRule.formJson(item);\n        var items = (item['items'] as List).map((e) => RewriteItem.fromJson(e)).toList();\n        await widget.requestRewrites.addRule(rule, items);\n      }\n      widget.requestRewrites.flushRequestRewriteConfig();\n\n      if (mounted) {\n        FlutterToastr.show(localizations.importSuccess, context);\n      }\n      setState(() {});\n    } catch (e, t) {\n      logger.e('导入失败 $file', error: e, stackTrace: t);\n      if (mounted) {\n        FlutterToastr.show(\"${localizations.importFailed} $e\", context);\n      }\n    }\n  }\n\n  void add([int currentIndex = -1]) {\n    Navigator.push(context, MaterialPageRoute(builder: (_) => const RewriteRule())).then((rule) {\n      if (rule != null) {\n        setState(() {});\n      }\n    });\n  }\n}\n\n///请求重写规则列表\nclass RequestRuleList extends StatefulWidget {\n  final RequestRewriteManager requestRewrites;\n\n  RequestRuleList(this.requestRewrites) : super(key: GlobalKey<_RequestRuleListState>());\n\n  @override\n  State<RequestRuleList> createState() => _RequestRuleListState();\n}\n\nclass _RequestRuleListState extends State<RequestRuleList> {\n  Set<int> selected = HashSet<int>();\n  late List<RequestRewriteRule> rules;\n  bool changed = false;\n\n  bool multiple = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  initState() {\n    super.initState();\n    rules = widget.requestRewrites.rules;\n  }\n\n  @override\n  void dispose() {\n    if (changed) {\n      widget.requestRewrites.flushRequestRewriteConfig();\n    }\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        persistentFooterButtons: multiple ? [globalMenu()] : null,\n        body: Container(\n            padding: const EdgeInsets.only(top: 10, bottom: 30),\n            decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n            child: Scrollbar(\n                child: ListView(\n              children: [\n                Row(\n                  mainAxisAlignment: MainAxisAlignment.start,\n                  children: [\n                    Container(width: 60, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)),\n                    SizedBox(width: 46, child: Text(localizations.enable, textAlign: TextAlign.center)),\n                    const VerticalDivider(),\n                    const Expanded(child: Text(\"URL\")),\n                    SizedBox(width: 60, child: Text(localizations.action, textAlign: TextAlign.center)),\n                  ],\n                ),\n                const Divider(thickness: 0.5),\n                Column(children: rows(widget.requestRewrites.rules))\n              ],\n            ))));\n  }\n\n  Stack globalMenu() {\n    return Stack(children: [\n      Container(\n          height: 50,\n          width: double.infinity,\n          margin: const EdgeInsets.only(top: 10),\n          decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2)))),\n      Positioned(\n          top: 0,\n          left: 0,\n          right: 0,\n          child: Center(\n              child: TextButton(\n                  onPressed: () {},\n                  child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [\n                    TextButton.icon(\n                        onPressed: () {\n                          export(context, selected.toList());\n                          setState(() {\n                            selected.clear();\n                            multiple = false;\n                          });\n                        },\n                        icon: const Icon(Icons.share, size: 18),\n                        label: Text(localizations.export, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () => removeRewrite(),\n                        icon: const Icon(Icons.delete, size: 18),\n                        label: Text(localizations.delete, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () {\n                          setState(() {\n                            multiple = false;\n                            selected.clear();\n                          });\n                        },\n                        icon: const Icon(Icons.cancel, size: 18),\n                        label: Text(localizations.cancel, style: const TextStyle(fontSize: 14))),\n                  ]))))\n    ]);\n  }\n\n  List<Widget> rows(List<RequestRewriteRule> list) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n    bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en');\n    return List.generate(list.length, (index) {\n      return InkWell(\n          highlightColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          hoverColor: primaryColor.withOpacity(0.3),\n          onLongPress: () => showMenus(index),\n          onTap: () async {\n            if (multiple) {\n              setState(() {\n                if (!selected.add(index)) {\n                  selected.remove(index);\n                }\n              });\n              return;\n            }\n            showEdit(index);\n          },\n          child: Container(\n              color: selected.contains(index)\n                  ? primaryColor.withOpacity(0.8)\n                  : index.isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 45,\n              padding: const EdgeInsets.all(5),\n              child: Row(\n                children: [\n                  SizedBox(\n                      width: 60,\n                      child: Text(list[index].name ?? \"\",\n                          overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13))),\n                  SizedBox(\n                      width: 35,\n                      child: SwitchWidget(\n                          scale: 0.65,\n                          value: list[index].enabled,\n                          onChanged: (val) {\n                            list[index].enabled = val;\n                            changed = true;\n                          })),\n                  const SizedBox(width: 20),\n                  Expanded(child: Text(list[index].url, style: const TextStyle(fontSize: 13))),\n                  const SizedBox(width: 3),\n                  SizedBox(\n                      width: 60,\n                      child: Text(isEN ? list[index].type.name.camelCaseToSpaced() : list[index].type.label,\n                          textAlign: TextAlign.center, style: const TextStyle(fontSize: 13))),\n                ],\n              )));\n    });\n  }\n\n  Future<void> showEdit(int index) async {\n    var rule = widget.requestRewrites.rules[index];\n    var rewriteItems = await widget.requestRewrites.getRewriteItems(rule);\n    if (!mounted) return;\n\n    Navigator.of(context)\n        .push(MaterialPageRoute(builder: (context) => RewriteRule(rule: rule, items: rewriteItems)))\n        .then((value) {\n      if (value != null && mounted) {\n        setState(() {});\n      }\n    });\n  }\n\n  //点击菜单\n  void showMenus(int index) {\n    setState(() {\n      selected.add(index);\n    });\n\n    showModalBottomSheet(\n        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),\n        context: context,\n        enableDrag: true,\n        builder: (ctx) {\n          return Wrap(alignment: WrapAlignment.center, children: [\n            BottomSheetItem(\n                text: localizations.multiple,\n                onPressed: () {\n                  setState(() => multiple = true);\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(text: localizations.edit, onPressed: () => showEdit(index)),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(text: localizations.share, onPressed: () => export(ctx, [index])),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: rules[index].enabled ? localizations.disabled : localizations.enable,\n                onPressed: () {\n                  rules[index].enabled = !rules[index].enabled;\n                  changed = true;\n                }),\n            const Divider(thickness: 0.5, height: 5),\n            BottomSheetItem(\n                text: localizations.delete,\n                onPressed: () async {\n                  await widget.requestRewrites.removeIndex([index]);\n                  widget.requestRewrites.flushRequestRewriteConfig();\n                  if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n                }),\n            Container(color: Theme.of(ctx).hoverColor, height: 8),\n            TextButton(\n                child: Container(\n                    height: 45,\n                    width: double.infinity,\n                    padding: const EdgeInsets.only(top: 10),\n                    child: Text(localizations.cancel, textAlign: TextAlign.center)),\n                onPressed: () {\n                  Navigator.of(ctx).pop();\n                }),\n          ]);\n        }).then((value) {\n      if (multiple) {\n        return;\n      }\n      setState(() {\n        selected.remove(index);\n      });\n    });\n  }\n\n  //导出\n  Future<void> export(BuildContext context, List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    String fileName = 'proxypin-rewrites.config';\n\n    var list = [];\n    for (var index in indexes) {\n      var rule = widget.requestRewrites.rules[index];\n      var json = rule.toJson();\n      json.remove(\"rewritePath\");\n      json['items'] = await widget.requestRewrites.getRewriteItems(rule);\n      list.add(json);\n    }\n\n    RenderBox? box;\n    if (await Platforms.isIpad() && context.mounted) {\n      box = context.findRenderObject() as RenderBox?;\n    }\n\n    final XFile file = XFile.fromData(utf8.encode(jsonEncode(list)), mimeType: 'config');\n    await SharePlus.instance\n        .share(ShareParams(files: [file], fileNameOverrides: [fileName], sharePositionOrigin: box?.paintBounds));\n  }\n\n  //删除\n  Future<void> removeRewrite() async {\n    if (selected.isEmpty) return;\n    return showConfirmDialog(context, content: localizations.requestRewriteDeleteConfirm(selected.length),\n        onConfirm: () async {\n      var list = selected.toList();\n      list.sort((a, b) => b.compareTo(a));\n      for (var value in list) {\n        await widget.requestRewrites.removeIndex([value]);\n      }\n      widget.requestRewrites.flushRequestRewriteConfig();\n      setState(() {\n        multiple = false;\n        selected.clear();\n      });\n      if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n    });\n  }\n}\n\n///请求重写规则添加\nclass RewriteRule extends StatefulWidget {\n  final RequestRewriteRule? rule;\n  final List<RewriteItem>? items;\n  final HttpRequest? request;\n\n  const RewriteRule({super.key, this.rule, this.items, this.request});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _RewriteRuleState();\n  }\n}\n\nclass _RewriteRuleState extends State<RewriteRule> {\n  final rewriteReplaceKey = GlobalKey<RewriteReplaceState>();\n  final rewriteUpdateKey = GlobalKey<RewriteUpdateState>();\n\n  late RequestRewriteRule rule;\n  List<RewriteItem>? items;\n  late RuleType ruleType;\n  late TextEditingController nameInput;\n  late TextEditingController urlInput;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  final ScrollController scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    rule = widget.rule ?? RequestRewriteRule(url: '', type: RuleType.responseReplace);\n    items = widget.items;\n    ruleType = rule.type;\n\n    nameInput = TextEditingController(text: rule.name);\n    urlInput = TextEditingController(text: rule.url);\n\n    if (items == null && widget.request != null) {\n      items = fromRequestItems(widget.request!, ruleType);\n    }\n  }\n\n  @override\n  void dispose() {\n    urlInput.dispose();\n    nameInput.dispose();\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    GlobalKey formKey = GlobalKey<FormState>();\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return Scaffold(\n        appBar: AppBar(\n          title: Row(children: [\n            Text(localizations.requestRewrite, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n            const SizedBox(width: 15),\n            Text.rich(TextSpan(\n                text: localizations.useGuide,\n                style: const TextStyle(color: Colors.blue, fontSize: 14),\n                recognizer: TapGestureRecognizer()\n                  ..onTap = () => launchUrl(\n                      mode: LaunchMode.externalApplication,\n                      Uri.parse(isCN\n                          ? 'https://gitee.com/wanghongenpin/proxypin/wikis/%E8%AF%B7%E6%B1%82%E9%87%8D%E5%86%99'\n                          : 'https://github.com/wanghongenpin/proxypin/wiki/Request-Rewrite')))),\n          ]),\n          actions: [\n            TextButton(\n                child: Text(localizations.save),\n                onPressed: () async {\n                  if (!(formKey.currentState as FormState).validate()) {\n                    FlutterToastr.show(localizations.cannotBeEmpty, context, position: FlutterToastr.center);\n                    return;\n                  }\n\n                  (formKey.currentState as FormState).save();\n                  rule.name = nameInput.text;\n                  rule.url = urlInput.text;\n                  items = rewriteReplaceKey.currentState?.getItems() ?? rewriteUpdateKey.currentState?.getItems();\n\n                  var requestRewrites = await RequestRewriteManager.instance;\n                  var index = requestRewrites.rules.indexOf(rule);\n\n                  if (index >= 0) {\n                    await requestRewrites.updateRule(index, rule, items);\n                  } else {\n                    await requestRewrites.addRule(rule, items!);\n                  }\n                  requestRewrites.flushRequestRewriteConfig();\n                  if (mounted) {\n                    FlutterToastr.show(localizations.saveSuccess, this.context);\n                    Navigator.of(this.context).pop(rule);\n                  }\n                })\n          ],\n        ),\n        body: Padding(\n          padding: const EdgeInsets.all(15),\n          child: NestedScrollView(\n              controller: scrollController,\n              headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {\n                return <Widget>[\n                  SliverToBoxAdapter(\n                      child: Form(\n                    key: formKey,\n                    child: Column(children: <Widget>[\n                      Row(children: [\n                        SizedBox(\n                            width: 60,\n                            child: Text('${localizations.enable}:',\n                                style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))),\n                        SwitchWidget(value: rule.enabled, onChanged: (val) => rule.enabled = val, scale: 0.8)\n                      ]),\n                      const SizedBox(height: 8),\n                      textField('${localizations.name}:', nameInput, localizations.pleaseEnter),\n                      const SizedBox(height: 8),\n                      // URL input with Method as prefix (method shown before the URL field)\n                      Row(children: [\n                        SizedBox(width: 60, child: Text('URL:', style: const TextStyle(fontSize: 16))),\n                        Expanded(\n                          child: TextFormField(\n                            controller: urlInput,\n                            validator: (val) => val?.isNotEmpty == true ? null : \"\",\n                            keyboardType: TextInputType.url,\n                            decoration: InputDecoration(\n                              hintText: 'https://www.example.com/api/*',\n                              hintStyle: TextStyle(color: Colors.grey.shade500),\n                              contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10),\n                              errorStyle: const TextStyle(height: 0, fontSize: 0),\n                              border: const OutlineInputBorder(),\n                              prefixIcon: Padding(\n                                padding: const EdgeInsets.only(left: 6, right: 6),\n                                child: MethodPopupMenu(\n                                  value: rule.method,\n                                  showSeparator: true,\n                                  onChanged: (val) {\n                                    setState(() {\n                                      rule.method = val;\n                                    });\n                                  },\n                                ),\n                              ),\n                            ),\n                          ),\n                        ),\n                      ]),\n                      const SizedBox(height: 8),\n                      Row(children: [\n                        SizedBox(\n                            width: 60,\n                            child: Text('${localizations.action}:',\n                                style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))),\n                        SizedBox(\n                            width: 165,\n                            height: 50,\n                            child: DropdownButtonFormField<RuleType>(\n                              onSaved: (val) => rule.type = val!,\n                              value: ruleType,\n                              decoration: const InputDecoration(\n                                  border: OutlineInputBorder(),\n                                  errorStyle: TextStyle(height: 0, fontSize: 0),\n                                  contentPadding: EdgeInsets.only(left: 5)),\n                              items: RuleType.values\n                                  .map((e) => DropdownMenuItem(value: e, child: Text(isCN ? e.label : e.name)))\n                                  .toList(),\n                              onChanged: onChangeType,\n                            )),\n                        const SizedBox(width: 10),\n                      ]),\n                      const SizedBox(height: 10),\n                    ]),\n                  ))\n                ];\n              },\n              body: rewriteRule()),\n        ));\n  }\n\n  void onChangeType(RuleType? val) async {\n    if (ruleType == val) return;\n\n    ruleType = val!;\n    items = [];\n\n    if (ruleType == widget.rule?.type) {\n      items = widget.items;\n    } else if (widget.request != null) {\n      items?.addAll(fromRequestItems(widget.request!, ruleType));\n    }\n\n    setState(() {\n      rewriteReplaceKey.currentState?.initItems(ruleType, items);\n      rewriteUpdateKey.currentState?.initItems(ruleType, items);\n    });\n  }\n\n  static List<RewriteItem> fromRequestItems(HttpRequest request, RuleType ruleType) {\n    if (ruleType == RuleType.requestReplace) {\n      //请求替换\n      return RewriteItem.fromRequest(request);\n    } else if (ruleType == RuleType.responseReplace && request.response != null) {\n      //响应替换\n      return RewriteItem.fromResponse(request.response!);\n    }\n    return [];\n  }\n\n  Widget rewriteRule() {\n    if (ruleType == RuleType.requestUpdate || ruleType == RuleType.responseUpdate) {\n      return MobileRewriteUpdate(key: rewriteUpdateKey, items: items, ruleType: ruleType, request: widget.request);\n    }\n\n    return MobileRewriteReplace(\n        scrollController: scrollController, key: rewriteReplaceKey, items: items, ruleType: ruleType);\n  }\n\n  Widget textField(String label, TextEditingController controller, String hint,\n      {bool required = false, TextInputType? keyboardType, FormFieldSetter<String>? onSaved}) {\n    return Row(children: [\n      SizedBox(width: 60, child: Text(label, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))),\n      Expanded(\n          child: TextFormField(\n        controller: controller,\n        validator: (val) => val?.isNotEmpty == true || !required ? null : \"\",\n        onSaved: onSaved,\n        keyboardType: keyboardType,\n        decoration: InputDecoration(\n          hintText: hint,\n          hintStyle: TextStyle(color: Colors.grey.shade500),\n          contentPadding: const EdgeInsets.only(left: 5),\n          errorStyle: const TextStyle(height: 0, fontSize: 0),\n          border: const OutlineInputBorder(),\n        ),\n      ))\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/rewrite/rewrite_replace.dart",
    "content": "﻿/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/ui/component/state_component.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/utils/lang.dart';\n\n/// 重写替换\n/// @author wanghongen\nclass MobileRewriteReplace extends StatefulWidget {\n  final RuleType ruleType;\n  final List<RewriteItem>? items;\n  final ScrollController? scrollController;\n\n  const MobileRewriteReplace({super.key, this.items, required this.ruleType, this.scrollController});\n\n  @override\n  State<MobileRewriteReplace> createState() => RewriteReplaceState();\n}\n\nclass RewriteReplaceState extends State<MobileRewriteReplace> {\n  final _headerKey = GlobalKey<HeadersState>();\n  final bodyTextController = TextEditingController();\n  late ScrollController? bodyScrollController;\n\n  late RuleType ruleType;\n  List<RewriteItem> items = [];\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  initState() {\n    super.initState();\n    initItems(widget.ruleType, widget.items);\n    bodyScrollController = trackingScroll(widget.scrollController);\n  }\n\n  @override\n  dispose() {\n    bodyTextController.dispose();\n    bodyScrollController?.dispose();\n    super.dispose();\n  }\n\n  ///初始化重写项\n  void initItems(RuleType ruleType, List<RewriteItem>? items) {\n    this.items.clear();\n    this.ruleType = ruleType;\n    if (ruleType == RuleType.redirect) {\n      _initRewriteItem(items, RewriteType.redirect, enabled: true);\n      return;\n    }\n\n    if (ruleType == RuleType.requestReplace) {\n      _initRewriteItem(items, RewriteType.replaceRequestLine);\n      _initRewriteItem(items, RewriteType.replaceRequestHeader);\n      _initRewriteItem(items, RewriteType.replaceRequestBody, enabled: true);\n      return;\n    }\n\n    if (ruleType == RuleType.responseReplace) {\n      _initRewriteItem(items, RewriteType.replaceResponseStatus);\n      _initRewriteItem(items, RewriteType.replaceResponseHeader);\n      _initRewriteItem(items, RewriteType.replaceResponseBody, enabled: true);\n      return;\n    }\n  }\n\n  void _initRewriteItem(List<RewriteItem>? items, RewriteType type, {bool enabled = false}) {\n    var item = items?.firstWhereOrNull((it) => it.type == type);\n    RewriteItem rewriteItem = RewriteItem(type, item?.enabled ?? enabled, values: item?.values);\n    this.items.add(rewriteItem);\n\n    if (type == RewriteType.replaceRequestHeader || type == RewriteType.replaceResponseHeader) {\n      _headerKey.currentState?.setHeaders(rewriteItem.headers);\n    }\n\n    if ((type == RewriteType.replaceResponseBody || type == RewriteType.replaceRequestBody) &&\n        rewriteItem.bodyType != ReplaceBodyType.file.name) {\n      bodyTextController.text = rewriteItem.body ?? '';\n    }\n  }\n\n  List<RewriteItem> getItems() {\n    var headers = _headerKey.currentState?.getHeaders();\n    if (headers != null) {\n      items\n          .firstWhere(\n              (item) => item.type == RewriteType.replaceRequestHeader || item.type == RewriteType.replaceResponseHeader)\n          .headers = headers;\n    }\n    return items;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (ruleType == RuleType.redirect) {\n      return Padding(padding: const EdgeInsets.only(top: 10, bottom: 10), child: redirectEdit(items.first));\n    }\n\n    if (ruleType == RuleType.responseReplace || ruleType == RuleType.requestReplace) {\n      bool requestEdited = ruleType == RuleType.requestReplace;\n      List<String> tabs = requestEdited\n          ? [localizations.requestLine, localizations.requestHeader, localizations.requestBody]\n          : [localizations.statusCode, localizations.responseHeader, localizations.responseBody];\n\n      return DefaultTabController(\n        length: tabs.length,\n        initialIndex: tabs.length - 1,\n        child: Scaffold(\n            appBar: tabBar(tabs),\n            body: TabBarView(children: [\n              KeepAliveWrapper(child: requestEdited ? requestLine() : statusCodeEdit()),\n              KeepAliveWrapper(child: headers()),\n              KeepAliveWrapper(child: body())\n            ])),\n      );\n    }\n\n    return Container();\n  }\n\n  //tabBar\n  TabBar tabBar(List<String> tabs) {\n    return TabBar(\n        labelPadding: const EdgeInsets.symmetric(horizontal: 0),\n        tabs: tabs\n            .map((label) => Tab(\n                height: 38,\n                child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [\n                  Text(label, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),\n                  const SizedBox(width: 3),\n                  Dot(color: items[tabs.indexOf(label)].enabled ? const Color(0xFF00FF00) : Colors.grey)\n                ])))\n            .toList());\n  }\n\n  bool jsonFormatted = false;\n\n  //body\n  Widget body() {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n    var rewriteItem = items.firstWhere(\n        (item) => item.type == RewriteType.replaceRequestBody || item.type == RewriteType.replaceResponseBody);\n\n    return ListView(physics: const ClampingScrollPhysics(), children: [\n      Row(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [\n        const SizedBox(width: 5),\n        Text(\"${localizations.type}: \"),\n        SizedBox(\n            width: 90,\n            child: DropdownButtonFormField<String>(\n                value: rewriteItem.bodyType ?? ReplaceBodyType.text.name,\n                focusColor: Colors.transparent,\n                itemHeight: 48,\n                decoration:\n                    const InputDecoration(contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none),\n                items: ReplaceBodyType.values\n                    .map((e) => DropdownMenuItem(\n                        value: e.name,\n                        child: Text(isCN ? e.label : e.name.toUpperCase(),\n                            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500))))\n                    .toList(),\n                onChanged: (val) => setState(() {\n                      rewriteItem.bodyType = val!;\n                    }))),\n        Expanded(\n            child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n          IconButton(\n            tooltip: 'JSON Format',\n            icon:\n                Icon(Icons.data_object, size: 20, color: jsonFormatted ? Theme.of(context).colorScheme.primary : null),\n            onPressed: () {\n              setState(() {\n                jsonFormatted = !jsonFormatted;\n                bodyTextController.text =\n                    jsonFormatted ? JSON.pretty(bodyTextController.text) : JSON.compact(bodyTextController.text);\n              });\n            },\n          ),\n          const SizedBox(width: 5),\n          Text(localizations.enable),\n          const SizedBox(width: 5),\n          SwitchWidget(\n              value: rewriteItem.enabled,\n              scale: 0.65,\n              onChanged: (val) => setState(() {\n                    rewriteItem.enabled = val;\n                  }))\n        ]))\n      ]),\n      const SizedBox(height: 10),\n      if (rewriteItem.bodyType == ReplaceBodyType.file.name)\n        fileBodyEdit(rewriteItem)\n      else\n        TextFormField(\n            controller: bodyTextController,\n            scrollPhysics: const BouncingScrollPhysics(),\n            scrollController: bodyScrollController,\n            style: const TextStyle(fontSize: 14),\n            minLines: 20,\n            maxLines: 23,\n            decoration: decoration(localizations.replaceBodyWith,\n                hintText: '${localizations.example} {\"code\":\"200\",\"data\":{}}'),\n            onChanged: (val) => rewriteItem.body = val)\n    ]);\n  }\n\n  Widget fileBodyEdit(RewriteItem item) {\n    return Column(children: [\n      const SizedBox(height: 5),\n      Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [\n        FilledButton(\n            onPressed: () async {\n              FilePickerResult? result = await FilePicker.platform.pickFiles();\n              if (result == null) {\n                return;\n              }\n              item.bodyFile = result.files.single.path;\n              setState(() {});\n            },\n            child: Text(localizations.selectFile, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500))),\n        const SizedBox(width: 10),\n        FilledButton(\n            onPressed: () {\n              setState(() {\n                item.bodyFile = null;\n              });\n            },\n            child: Text(localizations.delete, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500))),\n      ]),\n      const SizedBox(height: 10),\n      if (item.bodyFile != null)\n        Container(\n            padding: const EdgeInsets.all(8),\n            foregroundDecoration:\n                BoxDecoration(border: Border.all(color: Theme.of(context).colorScheme.primary, width: 1)),\n            child: Text(item.bodyFile ?? ''))\n    ]);\n  }\n\n  //headers\n  Widget headers() {\n    var rewriteItem = items.firstWhere(\n        (item) => item.type == RewriteType.replaceRequestHeader || item.type == RewriteType.replaceResponseHeader);\n\n    return ListView(physics: const ClampingScrollPhysics(), children: [\n      Row(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [\n        const Text('Header'),\n        const SizedBox(width: 10),\n        Expanded(\n            child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n          Text(localizations.enable),\n          const SizedBox(width: 10),\n          SwitchWidget(\n              value: rewriteItem.enabled,\n              scale: 0.65,\n              onChanged: (val) => setState(() {\n                    rewriteItem.enabled = val;\n                  }))\n        ]))\n      ]),\n      Headers(headers: rewriteItem.headers, key: _headerKey, scrollController: widget.scrollController)\n    ]);\n  }\n\n  ///请求行\n  Widget requestLine() {\n    var rewriteItem = items.firstWhere((item) => item.type == RewriteType.replaceRequestLine);\n    return ListView(\n      physics: const ClampingScrollPhysics(),\n      children: [\n        Row(children: [\n          Text(localizations.requestMethod),\n          const SizedBox(width: 10),\n          SizedBox(\n              width: 120,\n              child: DropdownButtonFormField<String>(\n                  value: rewriteItem.method?.name ?? 'GET',\n                  focusColor: Colors.transparent,\n                  itemHeight: 48,\n                  decoration: const InputDecoration(\n                      contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none),\n                  items: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']\n                      .map((e) => DropdownMenuItem(\n                          value: e, child: Text(e, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))))\n                      .toList(),\n                  onChanged: (val) {\n                    setState(() {\n                      rewriteItem.values['method'] = val!;\n                    });\n                  })),\n          Expanded(\n              child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n            Text(localizations.enable),\n            const SizedBox(width: 10),\n            SwitchWidget(\n                value: rewriteItem.enabled,\n                scale: 0.65,\n                onChanged: (val) {\n                  setState(() {\n                    rewriteItem.enabled = val;\n                  });\n                })\n          ])),\n        ]),\n        const SizedBox(height: 15),\n        textField(\"Path\", rewriteItem.path, \"${localizations.example} /api/v1/user\",\n            onChanged: (val) => rewriteItem.path = val),\n        const SizedBox(height: 15),\n        textField(\"URL${localizations.param}\", rewriteItem.queryParam, \"${localizations.example} id=1&name=2\",\n            onChanged: (val) => rewriteItem.queryParam = val),\n      ],\n    );\n  }\n\n  //重定向\n  Widget redirectEdit(RewriteItem rewriteItem) {\n    return TextFormField(\n        decoration: decoration(localizations.redirectTo, hintText: 'https://www.example.com/api'),\n        maxLines: 5,\n        initialValue: rewriteItem.redirectUrl,\n        onChanged: (val) => rewriteItem.redirectUrl = val,\n        validator: (val) {\n          if (val == null || val.trim().isEmpty) {\n            return '${localizations.redirect} URL ${localizations.cannotBeEmpty}';\n          }\n          return null;\n        });\n  }\n\n  Widget textField(String label, dynamic value, String hint, {ValueChanged<String>? onChanged}) {\n    return Row(children: [\n      SizedBox(width: 80, child: Text(label)),\n      Expanded(\n          child: TextFormField(\n        initialValue: value,\n        onChanged: onChanged,\n        decoration: InputDecoration(\n            hintText: hint,\n            hintStyle: TextStyle(color: Colors.grey.shade500),\n            contentPadding: const EdgeInsets.all(10),\n            errorStyle: const TextStyle(height: 0, fontSize: 0),\n            focusedBorder: focusedBorder(),\n            border: const OutlineInputBorder()),\n      ))\n    ]);\n  }\n\n  Widget statusCodeEdit() {\n    var rewriteItem = items.firstWhere((item) => item.type == RewriteType.replaceResponseStatus);\n\n    return ListView(physics: const ClampingScrollPhysics(), children: [\n      Row(crossAxisAlignment: CrossAxisAlignment.center, children: [\n        Text(localizations.statusCode),\n        const SizedBox(width: 10),\n        SizedBox(\n            width: 100,\n            child: TextFormField(\n              style: const TextStyle(fontSize: 14),\n              initialValue: rewriteItem.statusCode?.toString(),\n              onChanged: (val) => rewriteItem.statusCode = int.tryParse(val),\n              inputFormatters: [FilteringTextInputFormatter.digitsOnly],\n              decoration: InputDecoration(\n                  contentPadding: const EdgeInsets.all(10),\n                  focusedBorder: focusedBorder(),\n                  isDense: true,\n                  border: const OutlineInputBorder()),\n            )),\n        Expanded(\n            child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n          Text(localizations.enable),\n          const SizedBox(width: 10),\n          SwitchWidget(\n              value: rewriteItem.enabled,\n              scale: 0.65,\n              onChanged: (val) => setState(() {\n                    rewriteItem.enabled = val;\n                  }))\n        ])),\n        const SizedBox(width: 10),\n      ])\n    ]);\n  }\n\n  InputDecoration decoration(String label, {String? hintText}) {\n    Color color = Theme.of(context).colorScheme.primary;\n    return InputDecoration(\n        floatingLabelBehavior: FloatingLabelBehavior.always,\n        labelText: label,\n        hintText: hintText,\n        hintStyle: TextStyle(color: Colors.grey.shade500),\n        border: OutlineInputBorder(borderSide: BorderSide(width: 0.8, color: color)),\n        enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.5, color: color)),\n        focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color)));\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n\n///请求头\nclass Headers extends StatefulWidget {\n  final Map<String, String>? headers;\n  final ScrollController? scrollController;\n\n  const Headers({super.key, this.headers, this.scrollController});\n\n  @override\n  State<StatefulWidget> createState() {\n    return HeadersState();\n  }\n}\n\nclass HeadersState extends State<Headers> with AutomaticKeepAliveClientMixin {\n  final Map<TextEditingController, TextEditingController> _headers = {};\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.headers == null) {\n      return;\n    }\n\n    setHeaders(widget.headers);\n  }\n\n  void setHeaders(Map<String, String>? headers) {\n    _clear();\n    headers?.forEach((name, value) {\n      _headers[TextEditingController(text: name)] = TextEditingController(text: value);\n    });\n  }\n\n  ///获取所有请求头\n  Map<String, String> getHeaders() {\n    var headers = <String, String>{};\n    _headers.forEach((name, value) {\n      if (name.text.isEmpty) {\n        return;\n      }\n      headers[name.text] = value.text;\n    });\n    return headers;\n  }\n\n  @override\n  dispose() {\n    _clear();\n    super.dispose();\n  }\n\n  _clear() {\n    _headers.forEach((key, value) {\n      key.dispose();\n      value.dispose();\n    });\n    _headers.clear();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    var list = _buildRows();\n\n    return Padding(\n        padding: const EdgeInsets.only(top: 10),\n        child: ListView.separated(\n            shrinkWrap: true,\n            physics: const ClampingScrollPhysics(),\n            separatorBuilder: (context, index) =>\n                index == list.length ? const SizedBox() : const Divider(thickness: 0.2),\n            itemBuilder: (context, index) => index < list.length\n                ? list[index]\n                : TextButton(\n                    child: Text(\"${localizations.add}Header\", textAlign: TextAlign.center),\n                    onPressed: () {\n                      setState(() {\n                        _headers[TextEditingController()] = TextEditingController();\n                      });\n                    },\n                  ),\n            itemCount: list.length + 1));\n  }\n\n  List<Widget> _buildRows() {\n    List<Widget> list = [];\n\n    _headers.forEach((key, val) {\n      list.add(_row(\n          _cell(key, isKey: true),\n          _cell(val),\n          Padding(\n              padding: const EdgeInsets.only(right: 15),\n              child: InkWell(\n                  onTap: () {\n                    setState(() {\n                      _headers.remove(key);\n                    });\n                  },\n                  child: const Icon(Icons.remove_circle, size: 16)))));\n    });\n\n    return list;\n  }\n\n  Widget _cell(TextEditingController val, {bool isKey = false}) {\n    return Container(\n        padding: const EdgeInsets.only(right: 5),\n        child: TextFormField(\n            style: TextStyle(fontSize: 12, fontWeight: isKey ? FontWeight.w500 : null),\n            controller: val,\n            minLines: 1,\n            maxLines: 3,\n            decoration: InputDecoration(\n                isDense: true,\n                border: InputBorder.none,\n                hintStyle: TextStyle(fontSize: 12, color: Colors.grey),\n                hintText: isKey ? \"Key\" : \"Value\")));\n  }\n\n  Widget _row(Widget key, Widget val, Widget? op) {\n    return Row(children: [\n      Expanded(flex: 4, child: key),\n      const Text(\": \", style: TextStyle(color: Colors.deepOrangeAccent)),\n      Expanded(flex: 6, child: val),\n      op ?? const SizedBox()\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/rewrite/rewrite_update.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/components/manager/rewrite_rule.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/ui/component/text_field.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/utils/lang.dart';\n\nclass MobileRewriteUpdate extends StatefulWidget {\n  final RuleType ruleType;\n  final List<RewriteItem>? items;\n  final HttpRequest? request;\n\n  const MobileRewriteUpdate({super.key, required this.ruleType, this.items, required this.request});\n\n  @override\n  State<MobileRewriteUpdate> createState() => RewriteUpdateState();\n}\n\nclass RewriteUpdateState extends State<MobileRewriteUpdate> {\n  late RuleType ruleType;\n  List<RewriteItem> items = [];\n\n  AppLocalizations get i18n => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    initItems(widget.ruleType, widget.items);\n    // WidgetsBinding.instance.addPostFrameCallback((_) {\n    //   add();\n    // });\n  }\n\n  ///初始化重写项\n  initItems(RuleType ruleType, List<RewriteItem>? items) {\n    this.ruleType = ruleType;\n    this.items.clear();\n    if (items != null) {\n      this.items.addAll(items);\n    }\n  }\n\n  List<RewriteItem> getItems() {\n    return items;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      physics: ClampingScrollPhysics(),\n      children: [\n        Row(\n          children: [\n            SizedBox(\n                width: 260,\n                child: Text(i18n.requestRewriteRule,\n                    maxLines: 1, style: const TextStyle(fontSize: 13, color: Colors.grey))),\n            Expanded(\n                child: Row(\n              mainAxisAlignment: MainAxisAlignment.end,\n              children: [IconButton(onPressed: add, icon: const Icon(Icons.add)), const SizedBox(width: 10)],\n            ))\n          ],\n        ),\n        UpdateList(items: items, ruleType: ruleType, request: widget.request),\n      ],\n    );\n  }\n\n  add() {\n    Navigator.of(context)\n        .push(MaterialPageRoute(builder: (context) => RewriteUpdateEdit(ruleType: ruleType, request: widget.request)))\n        .then((value) {\n      if (value != null) {\n        setState(() {\n          items.add(value);\n        });\n      }\n    });\n  }\n}\n\nclass RewriteUpdateEdit extends StatefulWidget {\n  final RewriteItem? item;\n  final RuleType ruleType;\n  final HttpRequest? request;\n\n  const RewriteUpdateEdit({super.key, this.item, required this.ruleType, this.request});\n\n  @override\n  State<RewriteUpdateEdit> createState() => _RewriteUpdateAddState();\n}\n\nclass _RewriteUpdateAddState extends State<RewriteUpdateEdit> {\n  late RewriteType rewriteType;\n  GlobalKey formKey = GlobalKey<FormState>();\n  late RewriteItem rewriteItem;\n\n  var keyController = TextEditingController();\n  var valueController = TextEditingController();\n  var dataController = HighlightTextEditingController();\n\n  bool jsonFormatted = false;\n\n  AppLocalizations get i18n => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    rewriteType = widget.item?.type ?? RewriteType.updateBody;\n    rewriteItem = widget.item ?? RewriteItem(rewriteType, true);\n\n    keyController.text = rewriteItem.key ?? '';\n    valueController.text = rewriteItem.value ?? '';\n\n    initTestData();\n    keyController.addListener(onInputChangeMatch);\n    dataController.addListener(onInputChangeMatch);\n  }\n\n  @override\n  void dispose() {\n    keyController.dispose();\n    valueController.dispose();\n    dataController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isDelete = rewriteType == RewriteType.removeQueryParam || rewriteType == RewriteType.removeHeader;\n    bool isUpdate =\n        [RewriteType.updateBody, RewriteType.updateHeader, RewriteType.updateQueryParam].contains(rewriteType);\n\n    String keyTips = \"\";\n    String valueTips = \"\";\n    if (isDelete) {\n      keyTips = i18n.matchRule;\n      valueTips = i18n.emptyMatchAll;\n    } else if (rewriteType == RewriteType.updateQueryParam || rewriteType == RewriteType.updateHeader) {\n      keyTips = rewriteType == RewriteType.updateQueryParam ? \"name=123\" : \"Content-Type: application/json\";\n      valueTips = rewriteType == RewriteType.updateQueryParam ? \"name=456\" : \"Content-Type: application/xml\";\n    }\n\n    var typeList = widget.ruleType == RuleType.requestUpdate ? RewriteType.updateRequest : RewriteType.updateResponse;\n    bool isCN = Localizations.localeOf(context).languageCode == \"zh\";\n    return Scaffold(\n        appBar: AppBar(\n            centerTitle: true,\n            title: Text(i18n.requestRewriteRule, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),\n            actions: [\n              TextButton(\n                  onPressed: () {\n                    if (!(formKey.currentState as FormState).validate()) {\n                      FlutterToastr.show(i18n.cannotBeEmpty, context, position: FlutterToastr.center);\n                      return;\n                    }\n                    (formKey.currentState as FormState).save();\n                    rewriteItem.key = keyController.text;\n                    rewriteItem.value = valueController.text;\n                    rewriteItem.type = rewriteType;\n                    Navigator.of(context).pop(rewriteItem);\n                  },\n                  child: Text(i18n.confirm)),\n              SizedBox(width: 5)\n            ]),\n        body: Form(\n            key: formKey,\n            child: ListView(padding: const EdgeInsets.all(10), children: [\n              Row(\n                children: [\n                  Text(i18n.type),\n                  const SizedBox(width: 15),\n                  SizedBox(\n                      width: 140,\n                      child: DropdownButtonFormField<RewriteType>(\n                          value: rewriteType,\n                          focusColor: Colors.transparent,\n                          itemHeight: 48,\n                          decoration: const InputDecoration(\n                              contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none),\n                          items: typeList\n                              .map((e) => DropdownMenuItem(\n                                  value: e,\n                                  child: Text(e.getDescribe(isCN),\n                                      style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))))\n                              .toList(),\n                          onChanged: (val) {\n                            setState(() {\n                              rewriteType = val!;\n                            });\n                            initTestData();\n                          })),\n                ],\n              ),\n              const SizedBox(height: 15),\n              textField(isUpdate ? i18n.match : i18n.name, keyTips, controller: keyController, required: !isDelete),\n              const SizedBox(height: 15),\n              textField(isUpdate ? i18n.replace : i18n.value, valueTips, controller: valueController),\n              const SizedBox(height: 10),\n              Row(children: [\n                Align(\n                    alignment: Alignment.centerLeft, child: Text(i18n.testData, style: const TextStyle(fontSize: 14))),\n                const SizedBox(width: 10),\n                if (!isMatch) Text(i18n.noChangesDetected, style: TextStyle(color: Colors.red, fontSize: 14)),\n                Expanded(child: SizedBox()),\n                IconButton(\n                  tooltip: 'JSON Format',\n                  icon: Icon(Icons.data_object,\n                      size: 20, color: jsonFormatted ? Theme.of(context).colorScheme.primary : null),\n                  onPressed: () {\n                    setState(() {\n                      jsonFormatted = !jsonFormatted;\n                      dataController.text =\n                          jsonFormatted ? JSON.pretty(dataController.text) : JSON.compact(dataController.text);\n                    });\n                  },\n                ),\n                const SizedBox(width: 3),\n              ]),\n              const SizedBox(height: 5),\n              formField(i18n.enterMatchData, lines: 15, required: false, controller: dataController),\n            ])));\n  }\n\n  initTestData() {\n    dataController.splitPattern = null;\n    dataController.highlightEnabled = rewriteType != RewriteType.addQueryParam && rewriteType != RewriteType.addHeader;\n    bool isRemove = [RewriteType.removeHeader, RewriteType.removeQueryParam].contains(rewriteType);\n\n    valueController.removeListener(onInputChangeMatch);\n    if (isRemove) {\n      valueController.addListener(onInputChangeMatch);\n    }\n\n    if (widget.request == null) return;\n\n    if (rewriteType == RewriteType.updateBody) {\n      dataController.text = (widget.ruleType == RuleType.requestUpdate\n              ? widget.request?.getBodyString()\n              : widget.request?.response?.getBodyString()) ??\n          '';\n      return;\n    }\n\n    if (rewriteType == RewriteType.updateQueryParam || rewriteType == RewriteType.removeQueryParam) {\n      dataController.splitPattern = '&';\n      dataController.text = Uri.decodeQueryComponent(widget.request?.requestUri?.query ?? '');\n      return;\n    }\n\n    if (rewriteType == RewriteType.updateHeader || rewriteType == RewriteType.removeHeader) {\n      var headerData = widget.ruleType == RuleType.requestUpdate\n          ? widget.request?.headers.toRawHeaders()\n          : widget.request?.response?.headers.toRawHeaders();\n      dataController.text = headerData ?? '';\n      return;\n    }\n\n    dataController.clear();\n  }\n\n  bool onMatch = false; //是否正在匹配\n  bool isMatch = true;\n\n  onInputChangeMatch() {\n    if (onMatch || dataController.highlightEnabled == false) {\n      return;\n    }\n    onMatch = true;\n\n    //高亮显示\n    Future.delayed(const Duration(milliseconds: 600), () {\n      onMatch = false;\n      if (dataController.text.isEmpty) {\n        if (isMatch) return;\n        setState(() {\n          isMatch = true;\n        });\n        return;\n      }\n\n      if (!mounted) return;\n      setState(() {\n        bool isRemove = [RewriteType.removeHeader, RewriteType.removeQueryParam].contains(rewriteType);\n        String key = keyController.text;\n        if (isRemove && key.isNotEmpty) {\n          if (rewriteType == RewriteType.removeHeader) {\n            key = '$key: ';\n          } else {\n            key = '$key=';\n          }\n          key = '$key${valueController.text}';\n        }\n\n        var match = dataController.highlight(key,\n            caseSensitive: rewriteType != RewriteType.updateHeader && rewriteType != RewriteType.removeHeader);\n        isMatch = match;\n      });\n    });\n  }\n\n  Widget textField(String label, String hint, {bool required = false, int? lines, TextEditingController? controller}) {\n    return Row(children: [\n      SizedBox(width: 55, child: Text(label)),\n      Expanded(child: formField(hint, required: required, lines: lines, controller: controller))\n    ]);\n  }\n\n  Widget formField(String hint, {bool required = false, int? lines, TextEditingController? controller}) {\n    return TextFormField(\n      controller: controller,\n      style: const TextStyle(fontSize: 14),\n      onTapOutside: (event) => FocusScope.of(context).unfocus(),\n      minLines: lines ?? 1,\n      maxLines: lines ?? 3,\n      validator: (val) => val?.isNotEmpty == true || !required ? null : \"\",\n      decoration: InputDecoration(\n          hintText: hint,\n          hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),\n          contentPadding: const EdgeInsets.all(10),\n          errorStyle: const TextStyle(height: 0, fontSize: 0),\n          focusedBorder: focusedBorder(),\n          isDense: true,\n          border: const OutlineInputBorder()),\n    );\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n\nclass UpdateList extends StatefulWidget {\n  final List<RewriteItem> items;\n  final RuleType ruleType;\n  final HttpRequest? request;\n\n  const UpdateList({super.key, required this.items, required this.ruleType, this.request});\n\n  @override\n  State<UpdateList> createState() => _UpdateListState();\n}\n\nclass _UpdateListState extends State<UpdateList> {\n  AppLocalizations get i18n => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n        padding: const EdgeInsets.only(top: 10),\n        decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n        child: Column(children: [\n          Row(\n            mainAxisAlignment: MainAxisAlignment.start,\n            children: [\n              Container(width: 130, padding: const EdgeInsets.only(left: 10), child: Text(i18n.type)),\n              SizedBox(width: 50, child: Text(i18n.enable, textAlign: TextAlign.center)),\n              const VerticalDivider(),\n              Expanded(child: Text(i18n.modify)),\n            ],\n          ),\n          const Divider(thickness: 0.5),\n          Column(children: rows(widget.items))\n        ]));\n  }\n\n  int selected = -1;\n\n  List<Widget> rows(List<RewriteItem> list) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n\n    return List.generate(list.length, (index) {\n      return InkWell(\n          highlightColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          hoverColor: primaryColor.withOpacity(0.3),\n          onTap: () => Navigator.push(\n                      context,\n                      MaterialPageRoute(\n                          builder: (context) =>\n                              RewriteUpdateEdit(item: list[index], ruleType: widget.ruleType, request: widget.request)))\n                  .then((value) {\n                if (value != null) setState(() {});\n              }),\n          onLongPress: () => showMenus(index),\n          child: Container(\n              color: selected == index\n                  ? primaryColor\n                  : index.isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              constraints: const BoxConstraints(minHeight: 38, maxHeight: 45),\n              padding: const EdgeInsets.all(5),\n              child: Row(\n                children: [\n                  SizedBox(\n                      width: 130,\n                      child: Text(list[index].type.getDescribe(i18n.localeName == 'zh'),\n                          style: const TextStyle(fontSize: 13))),\n                  SizedBox(\n                      width: 40,\n                      child: SwitchWidget(\n                          scale: 0.6,\n                          value: list[index].enabled,\n                          onChanged: (val) {\n                            list[index].enabled = val;\n                          })),\n                  const SizedBox(width: 20),\n                  Expanded(\n                      child:\n                          Text(getText(list[index]).fixAutoLines(), maxLines: 2, style: const TextStyle(fontSize: 13))),\n                ],\n              )));\n    });\n  }\n\n  String getText(RewriteItem item) {\n    bool isUpdate =\n        [RewriteType.updateBody, RewriteType.updateHeader, RewriteType.updateQueryParam].contains(item.type);\n    if (isUpdate) {\n      return \"${item.key} -> ${item.value}\";\n    }\n\n    return \"${item.key}=${item.value}\";\n  }\n\n  showMenus(int index) {\n    setState(() {\n      selected = index;\n    });\n\n    showModalBottomSheet(\n        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),\n        context: context,\n        enableDrag: true,\n        builder: (ctx) {\n          return Wrap(alignment: WrapAlignment.center, children: [\n            BottomSheetItem(\n                text: i18n.modify,\n                onPressed: () {\n                  Navigator.push(\n                      context,\n                      MaterialPageRoute(\n                          builder: (BuildContext context) => RewriteUpdateEdit(\n                              item: widget.items[index],\n                              ruleType: widget.ruleType,\n                              request: widget.request))).then((value) {\n                    if (value != null) {\n                      setState(() {});\n                    }\n                  });\n                }),\n            const Divider(thickness: 0.5),\n            BottomSheetItem(\n                text: widget.items[index].enabled ? i18n.disabled : i18n.enable,\n                onPressed: () => widget.items[index].enabled = !widget.items[index].enabled),\n            const Divider(thickness: 0.5),\n            BottomSheetItem(\n                text: i18n.delete,\n                onPressed: () async {\n                  widget.items.removeAt(index);\n                  if (mounted) FlutterToastr.show(i18n.deleteSuccess, context);\n                }),\n            Container(color: Theme.of(context).hoverColor, height: 8),\n            TextButton(\n                child: Container(\n                    height: 50,\n                    width: double.infinity,\n                    padding: const EdgeInsets.only(top: 10),\n                    child: Text(i18n.cancel, textAlign: TextAlign.center)),\n                onPressed: () {\n                  Navigator.of(context).pop();\n                }),\n          ]);\n        }).then((value) {\n      setState(() {\n        selected = -1;\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/script.dart",
    "content": "﻿/*\n * Copyright 2023 Hongen Wang All rights reserved.\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 */\nimport 'dart:convert';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_code_editor/flutter_code_editor.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:get/get.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_highlight/themes/monokai-sublime.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:highlight/languages/javascript.dart';\nimport 'package:proxypin/network/components/manager/script_manager.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/ui/mobile/widgets/floating_window.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:share_plus/share_plus.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\n/// @author wanghongen\n/// 2023/10/19\n/// js脚本\nclass MobileScript extends StatefulWidget {\n  const MobileScript({super.key});\n\n  @override\n  State<StatefulWidget> createState() => _MobileScriptState();\n}\n\nbool _refresh = false;\n\n/// 刷新脚本\nvoid _refreshScript({bool force = false}) {\n  if (_refresh && !force) {\n    return;\n  }\n  _refresh = true;\n  Future.delayed(const Duration(milliseconds: 1500), () async {\n    _refresh = false;\n    (await ScriptManager.instance).flushConfig();\n  });\n}\n\nclass _MobileScriptState extends State<MobileScript> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(title: Text(localizations.script, style: const TextStyle(fontSize: 16))),\n        body: Padding(\n            padding: const EdgeInsets.only(left: 15, right: 10),\n            child: futureWidget(\n                ScriptManager.instance,\n                loading: true,\n                (data) => Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        mainAxisAlignment: MainAxisAlignment.start,\n                        children: [\n                          SizedBox(\n                              child: ListTile(\n                                  title: Text(localizations.enableScript),\n                                  subtitle: Text(localizations.scriptUseDescribe),\n                                  trailing: SwitchWidget(\n                                    value: data.enabled,\n                                    onChanged: (value) {\n                                      data.enabled = value;\n                                      _refreshScript();\n                                    },\n                                  ))),\n                          const SizedBox(height: 8),\n                          Row(\n                            mainAxisAlignment: MainAxisAlignment.end,\n                            children: [\n                              TextButton.icon(\n                                  icon: const Icon(Icons.add, size: 18),\n                                  onPressed: scriptEdit,\n                                  label: Text(localizations.add)),\n                              const SizedBox(width: 5),\n                              TextButton.icon(\n                                icon: const Icon(Icons.input_rounded, size: 18),\n                                onPressed: import,\n                                label: Text(localizations.import),\n                              ),\n                              const SizedBox(width: 5),\n                              TextButton.icon(\n                                icon: const Icon(Icons.terminal, size: 18),\n                                onPressed: consoleLog,\n                                label: Text(localizations.logger),\n                              ),\n                            ],\n                          ),\n                          const SizedBox(height: 5),\n                          Expanded(child: ScriptList(scripts: data.list)),\n                        ]))));\n  }\n\n  void consoleLog() {\n    // FloatingWindowManager().show(context);\n    Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ScriptConsoleLog()));\n  }\n\n  //导入js\n  Future<void> import() async {\n    FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any);\n    if (result == null || result.files.isEmpty) {\n      return;\n    }\n    var file = result.files.single.xFile;\n    try {\n      var scriptManager = (await ScriptManager.instance);\n      var json = jsonDecode(utf8.decode(await file.readAsBytes()));\n\n      if (json is List<dynamic>) {\n        for (var item in json) {\n          var scriptItem = ScriptItem.fromJson(item);\n          await scriptManager.addScript(scriptItem, item['script']);\n        }\n      } else {\n        var scriptItem = ScriptItem.fromJson(json);\n        await scriptManager.addScript(scriptItem, json['script']);\n      }\n\n      _refreshScript();\n      if (mounted) {\n        FlutterToastr.show(localizations.importSuccess, context);\n      }\n      setState(() {});\n    } catch (e, t) {\n      logger.e('导入失败 $file', error: e, stackTrace: t);\n      if (mounted) {\n        FlutterToastr.show(\"${localizations.importFailed} $e\", context);\n      }\n    }\n  }\n\n  /// 添加脚本\n  Future<void> scriptEdit() async {\n    Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ScriptEdit())).then((value) {\n      if (value != null) {\n        setState(() {});\n      }\n    });\n  }\n}\n\n///控制台日志\nclass ScriptConsoleLog extends StatefulWidget {\n  const ScriptConsoleLog({super.key});\n\n  @override\n  State<StatefulWidget> createState() => _ScriptConsoleLogState();\n}\n\nclass _ScriptConsoleLogState extends State<ScriptConsoleLog> {\n  int channelId = \"ScriptConsoleLog\".hashCode;\n\n  static final List<LogInfo> logs = [];\n  static FloatingWindowManager floatingWindowManager = FloatingWindowManager();\n\n  final ScrollController _scrollController = ScrollController();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback((d) {\n      if (_scrollController.hasClients) {\n        _scrollController.jumpTo(_scrollController.position.maxScrollExtent);\n      }\n    });\n\n    LogHandler logHandler = LogHandler(\n        channelId: channelId,\n        handle: (log) {\n          logs.add(log);\n\n          if (!mounted && !floatingWindowManager.isShow) {\n            logs.clear();\n            ScriptManager.removeLogHandler(channelId);\n            return;\n          }\n\n          if (mounted) {\n            setState(() {});\n          }\n        });\n\n    ScriptManager.registerLogHandler(logHandler);\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    if (!floatingWindowManager.isShow) {\n      logs.clear();\n      ScriptManager.removeLogHandler(channelId);\n    }\n    _scrollController.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(title: Text(localizations.logger, style: const TextStyle(fontSize: 16)), actions: [\n          IconButton(\n              tooltip: localizations.windowMode,\n              onPressed: () {\n                if (floatingWindowManager.isShow) {\n                  floatingWindowManager.hide();\n                  return;\n                }\n                floatingWindowManager.show(context,\n                    widget: ScriptLogSmallWindow(floatingWindowManager: floatingWindowManager));\n              },\n              icon: const Icon(Icons.picture_in_picture_alt_rounded)),\n          const SizedBox(width: 5),\n          IconButton(\n              tooltip: localizations.clear,\n              onPressed: () => setState(() {\n                    logs.clear();\n                  }),\n              icon: const Icon(Icons.delete)),\n          const SizedBox(width: 10)\n        ]),\n        body: Container(\n          padding: const EdgeInsets.only(top: 10, bottom: 10, right: 3),\n          decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n          child: Scrollbar(\n              controller: _scrollController,\n              thumbVisibility: true,\n              thickness: 6,\n              interactive: true,\n              child: loggerContent()),\n        ));\n  }\n\n  Widget loggerContent() {\n    return ListView.builder(\n        controller: _scrollController,\n        itemCount: logs.length,\n        itemBuilder: (context, index) {\n          var log = logs[index];\n          Color? color;\n          if (log.level == 'error') {\n            color = Colors.red;\n          } else if (log.level == 'warn') {\n            color = Colors.orange;\n          }\n\n          return Padding(\n              padding: const EdgeInsets.only(bottom: 5, left: 3, right: 3),\n              child: Row(\n                children: [\n                  Text(log.time.timeFormat(), style: const TextStyle(fontSize: 13, color: Colors.grey)),\n                  const SizedBox(width: 8),\n                  Text(log.level, style: TextStyle(fontSize: 13, color: color)),\n                  const SizedBox(width: 8),\n                  Expanded(child: SelectableText(log.output, style: TextStyle(fontSize: 13, color: color))),\n                ],\n              ));\n        });\n  }\n}\n\nclass ScriptLogSmallWindow extends StatefulWidget {\n  final FloatingWindowManager floatingWindowManager;\n\n  const ScriptLogSmallWindow({super.key, required this.floatingWindowManager});\n\n  @override\n  State<StatefulWidget> createState() => _ScriptLogSmallWindowState();\n}\n\nclass _ScriptLogSmallWindowState extends State<ScriptLogSmallWindow> {\n  final List<LogInfo> logs = [];\n  final ScrollController _scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    LogHandler logHandler = LogHandler(\n        channelId: hashCode,\n        handle: (log) {\n          logs.add(log);\n          if (!mounted) {\n            ScriptManager.removeLogHandler(hashCode);\n            return;\n          }\n          setState(() {});\n          _scrollController.jumpTo(_scrollController.position.maxScrollExtent);\n        });\n    ScriptManager.registerLogHandler(logHandler);\n  }\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    logger.d(\"dispose small window log handler $hashCode\");\n    ScriptManager.removeLogHandler(hashCode);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FloatingWindow(\n        top: 320,\n        right: 8,\n        child: Material(\n            child: Container(\n                height: 320,\n                width: 180,\n                decoration: BoxDecoration(\n                    color: Colors.teal.withOpacity(0.3),\n                    border: Border.all(color: Colors.grey.withOpacity(0.8)),\n                    borderRadius: const BorderRadius.all(Radius.circular(10))),\n                child: Stack(\n                  children: [\n                    Positioned(\n                        top: -12,\n                        left: -5,\n                        child: IconButton(\n                            onPressed: () {\n                              Navigator.of(context)\n                                  .push(MaterialPageRoute(builder: (context) => const ScriptConsoleLog()));\n                            },\n                            icon: const Icon(Icons.picture_in_picture, size: 20))),\n                    Positioned(\n                        top: -12,\n                        right: -8,\n                        child: IconButton(\n                            onPressed: () => widget.floatingWindowManager.hide(),\n                            icon: const Icon(Icons.close, size: 20))),\n                    list()\n                  ],\n                ))));\n  }\n\n  Widget list() {\n    return Padding(\n        padding: const EdgeInsets.only(bottom: 5, top: 18),\n        child: Scrollbar(\n            controller: _scrollController,\n            thumbVisibility: true,\n            thickness: 2,\n            child: ListView.builder(\n                controller: _scrollController,\n                itemCount: logs.length,\n                itemBuilder: (context, index) {\n                  var log = logs[index];\n                  return Padding(\n                      padding: const EdgeInsets.only(bottom: 3, left: 3, right: 3),\n                      child: Text(log.output,\n                          maxLines: 3,\n                          overflow: TextOverflow.ellipsis,\n                          style: TextStyle(fontSize: 13, color: log.level == 'error' ? Colors.red : null)));\n                })));\n  }\n}\n\n/// 编辑脚本\nclass ScriptEdit extends StatefulWidget {\n  final ScriptItem? scriptItem;\n  final String? script;\n  final String? url;\n  final List<String>? urls;\n  final String? title;\n  final bool fromRemoteUrl;\n\n  const ScriptEdit({\n    super.key,\n    this.scriptItem,\n    this.script,\n    this.url,\n    this.urls,\n    this.title,\n    this.fromRemoteUrl = false,\n  });\n\n  @override\n  State<StatefulWidget> createState() => _ScriptEditState();\n}\n\nclass _ScriptEditState extends State<ScriptEdit> {\n  late CodeController script;\n  late TextEditingController nameController;\n  late List<TextEditingController> urlControllers;\n  late TextEditingController remoteUrlController;\n  late bool _useRemote;\n  final RxBool _fetchingRemoteScript = false.obs;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    final urls = widget.scriptItem?.urls ??\n        (widget.urls != null && widget.urls!.isNotEmpty\n            ? widget.urls!\n            : (widget.url != null && widget.url!.isNotEmpty ? [widget.url!] : <String>[]));\n    urlControllers =\n        urls.isNotEmpty ? urls.map((u) => TextEditingController(text: u)).toList() : [TextEditingController()];\n    script = CodeController(language: javascript, text: widget.script ?? ScriptManager.template);\n    nameController = TextEditingController(text: widget.scriptItem?.name ?? widget.title ?? '');\n    remoteUrlController = TextEditingController(text: widget.scriptItem?.remoteUrl ?? '');\n    _useRemote = widget.fromRemoteUrl || ((widget.scriptItem?.remoteUrl ?? '').trim().isNotEmpty);\n  }\n\n  @override\n  void dispose() {\n    for (final c in urlControllers) {\n      c.dispose();\n    }\n    script.dispose();\n    nameController.dispose();\n    remoteUrlController.dispose();\n    _fetchingRemoteScript.close();\n    super.dispose();\n  }\n\n  Future<void> _fetchRemoteScript() async {\n    if (_fetchingRemoteScript.value) return;\n    final remoteUrl = remoteUrlController.text.trim();\n    if (remoteUrl.isEmpty) {\n      FlutterToastr.show(\"${localizations.remoteUrl} ${localizations.cannotBeEmpty}\", context,\n          position: FlutterToastr.top);\n      return;\n    }\n\n    final uri = Uri.tryParse(remoteUrl);\n    if (uri == null || !(uri.scheme == 'http' || uri.scheme == 'https')) {\n      FlutterToastr.show(\"${localizations.remoteUrl} ${localizations.fail}\", context, position: FlutterToastr.top);\n      return;\n    }\n\n    try {\n      _fetchingRemoteScript.value = true;\n      final resp = await http.get(uri);\n      if (resp.statusCode < 200 || resp.statusCode >= 300) {\n        FlutterToastr.show(\"Fetch failed: HTTP ${resp.statusCode}\", context, position: FlutterToastr.top);\n        return;\n      }\n      final content = utf8.decode(resp.bodyBytes);\n      script.text = content;\n      if (mounted) {\n        setState(() {});\n      }\n    } catch (e) {\n      if (mounted) {\n        FlutterToastr.show(\"Fetch failed: $e\", context, position: FlutterToastr.top);\n      }\n    } finally {\n      _fetchingRemoteScript.value = false;\n    }\n  }\n\n  void _resetScript() {\n    script.text = ScriptManager.template;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    GlobalKey formKey = GlobalKey<FormState>();\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return Scaffold(\n        appBar: AppBar(\n            title: Row(children: [\n              Text(localizations.scriptEdit, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n              const SizedBox(width: 10),\n              Text.rich(TextSpan(\n                  text: localizations.useGuide,\n                  style: const TextStyle(color: Colors.blue, fontSize: 14),\n                  recognizer: TapGestureRecognizer()\n                    ..onTap = () => launchUrl(\n                        mode: LaunchMode.externalApplication,\n                        Uri.parse(isCN\n                            ? 'https://gitee.com/wanghongenpin/proxypin/wikis/%E8%84%9A%E6%9C%AC'\n                            : 'https://github.com/wanghongenpin/proxypin/wiki/Script')))),\n            ]),\n            actions: [\n              TextButton(\n                  onPressed: () async {\n                    if (!(formKey.currentState as FormState).validate()) {\n                      FlutterToastr.show(\"${localizations.name} URL ${localizations.cannotBeEmpty}\", context,\n                          position: FlutterToastr.top);\n                      return;\n                    }\n                    // 收集所有非空、去重的 url\n                    final urls = urlControllers.map((c) => c.text.trim()).where((u) => u.isNotEmpty).toSet().toList();\n                    if (urls.isEmpty) {\n                      FlutterToastr.show(\"URL ${localizations.cannotBeEmpty}\", context, position: FlutterToastr.top);\n                      return;\n                    }\n\n                    // Only persist remoteUrl when remote mode is enabled.\n                    final remoteUrl = _useRemote ? remoteUrlController.text.trim() : '';\n                    final hasRemote = remoteUrl.isNotEmpty;\n                    if (_useRemote && !hasRemote) {\n                      FlutterToastr.show(\"Remote URL ${localizations.cannotBeEmpty}\", context,\n                          position: FlutterToastr.top);\n                      return;\n                    }\n\n                    var scriptManager = await ScriptManager.instance;\n                    if (widget.scriptItem == null) {\n                      var scriptItem = ScriptItem(true, nameController.text, urls);\n                      scriptItem.remoteUrl = _useRemote ? remoteUrl : null;\n                      await scriptManager.addScript(scriptItem, script.text);\n                    } else {\n                      widget.scriptItem?.name = nameController.text;\n                      widget.scriptItem?.urls = urls;\n                      widget.scriptItem?.urlRegs = null;\n                      widget.scriptItem?.remoteUrl = _useRemote ? remoteUrl : null;\n                      await scriptManager.updateScript(widget.scriptItem!, script.text);\n                    }\n\n                    _refreshScript(force: true);\n                    if (context.mounted) {\n                      FlutterToastr.show(localizations.saveSuccess, context);\n                      Navigator.of(context).maybePop(true);\n                    }\n                  },\n                  child: Text(localizations.save)),\n            ]),\n        body: Form(\n            key: formKey,\n            child: ListView(\n              padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),\n              children: [\n                // Name section\n                Card(\n                    color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),\n                    elevation: 0,\n                    shape: RoundedRectangleBorder(\n                        side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)),\n                        borderRadius: BorderRadius.circular(8)),\n                    child: Padding(\n                        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),\n                        child: textField(\"${localizations.name}:\", nameController, localizations.pleaseEnter))),\n\n                // URLs section\n                Card(\n                    color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),\n                    elevation: 0,\n                    shape: RoundedRectangleBorder(\n                        side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)),\n                        borderRadius: BorderRadius.circular(8)),\n                    child: Padding(\n                        padding: const EdgeInsets.symmetric(horizontal: 10),\n                        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n                          Row(children: [\n                            const Text(\"URL(s):\"),\n                            const SizedBox(width: 8),\n                            IconButton(\n                                icon: const Icon(Icons.add_outlined, size: 20),\n                                tooltip: localizations.add,\n                                onPressed: () => setState(() => urlControllers.add(TextEditingController()))),\n                            const Spacer(),\n                            Text(\"${urlControllers.length}\", style: const TextStyle(fontSize: 12, color: Colors.grey))\n                          ]),\n                          const SizedBox(height: 6),\n                          ...List.generate(\n                              urlControllers.length,\n                              (i) => Padding(\n                                  padding: const EdgeInsets.only(bottom: 8),\n                                  child: Row(children: [\n                                    Expanded(\n                                        child: TextFormField(\n                                      controller: urlControllers[i],\n                                      validator: (val) => val?.isNotEmpty == true ? null : \"\",\n                                      keyboardType: TextInputType.url,\n                                      decoration: InputDecoration(\n                                        hintText: \"github.com/api/*\",\n                                        hintStyle: const TextStyle(fontSize: 14, color: Colors.grey),\n                                        contentPadding: const EdgeInsets.all(10),\n                                        errorStyle: const TextStyle(height: 0, fontSize: 0),\n                                        focusedBorder: focusedBorder(),\n                                        isDense: true,\n                                        border: const OutlineInputBorder(),\n                                      ),\n                                    )),\n                                    if (urlControllers.length > 1)\n                                      IconButton(\n                                          icon: const Icon(Icons.remove_circle_outline, color: Colors.red),\n                                          tooltip: localizations.delete,\n                                          onPressed: () {\n                                            setState(() {\n                                              urlControllers[i].dispose();\n                                              urlControllers.removeAt(i);\n                                            });\n                                          }),\n                                  ])))\n                        ]))),\n\n                // Source section\n                Card(\n                    color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),\n                    elevation: 0,\n                    shape: RoundedRectangleBorder(\n                        side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)),\n                        borderRadius: BorderRadius.circular(8)),\n                    child: Padding(\n                        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),\n                        child: Row(children: [\n                          SizedBox(width: 55, child: Text('${localizations.type}:')),\n                          Expanded(\n                              child: DropdownButtonFormField<bool>(\n                            initialValue: _useRemote,\n                            items: [\n                              DropdownMenuItem(value: false, child: Text(localizations.local)),\n                              DropdownMenuItem(value: true, child: Text(localizations.remoteUrl)),\n                            ],\n                            onChanged: (val) {\n                              if (val == null) return;\n                              setState(() {\n                                _useRemote = val;\n                              });\n                            },\n                            decoration: InputDecoration(\n                              contentPadding: const EdgeInsets.all(10),\n                              focusedBorder: focusedBorder(),\n                              isDense: true,\n                              border: const OutlineInputBorder(),\n                            ),\n                          ))\n                        ]))),\n\n                // Remote URL section\n                if (_useRemote)\n                  Card(\n                      color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),\n                      elevation: 0,\n                      shape: RoundedRectangleBorder(\n                          side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)),\n                          borderRadius: BorderRadius.circular(8)),\n                      child: Padding(\n                          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),\n                          child: Row(children: [\n                            SizedBox(width: 65, child: Text('${localizations.remoteUrl}:')),\n                            Expanded(\n                              child: SizedBox(\n                                height: 34,\n                                child: TextFormField(\n                                  controller: remoteUrlController,\n                                  keyboardType: TextInputType.url,\n                                  decoration: InputDecoration(\n                                    hintText: 'https://example.com/script.js',\n                                    hintStyle: const TextStyle(fontSize: 14, color: Colors.grey),\n                                    contentPadding: const EdgeInsets.all(10),\n                                    focusedBorder: focusedBorder(),\n                                    isDense: true,\n                                    border: const OutlineInputBorder(),\n                                  ),\n                                  onFieldSubmitted: (_) => _fetchRemoteScript(),\n                                ),\n                              ),\n                            ),\n                            const SizedBox(width: 3),\n                            Obx(() {\n                              // Keep the button visually aligned with the text field by fixing the height\n                              // and using a compact FilledButton (with icon when idle and spinner when fetching).\n                              return SizedBox(\n                                height: 34,\n                                child: Tooltip(\n                                  message: localizations.view,\n                                  child: FilledButton.tonal(\n                                    onPressed: _fetchRemoteScript,\n                                    style: FilledButton.styleFrom(\n                                        minimumSize: const Size(44, 34),\n                                        padding: const EdgeInsets.symmetric(horizontal: 8),\n                                        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6))),\n                                    child: _fetchingRemoteScript.value\n                                        ? const SizedBox(\n                                            width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))\n                                        : const Icon(Icons.cloud_download, size: 18),\n                                  ),\n                                ),\n                              );\n                            }),\n                          ]))),\n\n                // Script section\n                Card(\n                    color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),\n                    elevation: 0,\n                    shape: RoundedRectangleBorder(\n                        side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)),\n                        borderRadius: BorderRadius.circular(8)),\n                    child: Padding(\n                        padding: const EdgeInsets.symmetric(horizontal: 10),\n                        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n                          Row(children: [\n                            Text(\"${localizations.script}:\", style: const TextStyle(fontWeight: FontWeight.w500)),\n                            const Spacer(),\n                            Tooltip(\n                                message: localizations.copy,\n                                child: IconButton(\n                                    icon: const Icon(Icons.copy_all_outlined, size: 20),\n                                    onPressed: () {\n                                      Clipboard.setData(ClipboardData(text: script.text));\n                                      FlutterToastr.show(localizations.copied, context, position: FlutterToastr.top);\n                                    })),\n                            Tooltip(\n                                message: 'Reset',\n                                child: IconButton(\n                                    icon: const Icon(Icons.settings_backup_restore, size: 22),\n                                    onPressed: _resetScript)),\n                            Tooltip(\n                                message: localizations.clear,\n                                child: IconButton(\n                                    icon: const Icon(Icons.delete_sweep_outlined, size: 22),\n                                    onPressed: () {\n                                      script.text = '';\n                                      setState(() {});\n                                    }))\n                          ]),\n                          CodeTheme(\n                              data: CodeThemeData(styles: monokaiSublimeTheme),\n                              child: ClipRRect(\n                                  borderRadius: BorderRadius.circular(6),\n                                  child: Container(\n                                      decoration: BoxDecoration(\n                                          color: Colors.grey.shade900,\n                                          border: Border.all(color: Colors.grey.withOpacity(0.2))),\n                                      child: SingleChildScrollView(\n                                          child: CodeField(\n                                        readOnly: _useRemote,\n                                        enableSuggestions: true,\n                                        textStyle: const TextStyle(fontSize: 13, color: Colors.white),\n                                        controller: script,\n                                        gutterStyle: const GutterStyle(width: 50, margin: 0),\n                                      ))))),\n                        ]))),\n              ],\n            )));\n  }\n\n  Widget textField(String label, TextEditingController controller, String hint, {TextInputType? keyboardType}) {\n    return Row(children: [\n      SizedBox(width: 65, child: Text(label)),\n      Expanded(\n          child: TextFormField(\n        controller: controller,\n        validator: (val) => val?.isNotEmpty == true ? null : \"\",\n        keyboardType: keyboardType,\n        decoration: InputDecoration(\n            hintText: hint,\n            contentPadding: const EdgeInsets.all(10),\n            hintStyle: const TextStyle(fontSize: 14, color: Colors.grey),\n            errorStyle: const TextStyle(height: 0, fontSize: 0),\n            focusedBorder: focusedBorder(),\n            isDense: true,\n            border: const OutlineInputBorder()),\n      ))\n    ]);\n  }\n\n  InputBorder focusedBorder() {\n    return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2));\n  }\n}\n\n/// 脚本列表\nclass ScriptList extends StatefulWidget {\n  final List<ScriptItem> scripts;\n\n  const ScriptList({super.key, required this.scripts});\n\n  @override\n  State<ScriptList> createState() => _ScriptListState();\n}\n\nclass _ScriptListState extends State<ScriptList> {\n  Set<int> selected = {};\n  bool multiple = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        persistentFooterButtons: multiple ? [globalMenu()] : null,\n        body: Container(\n            padding: const EdgeInsets.only(top: 10, bottom: 30),\n            decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),\n            child: Scrollbar(\n                child: ListView(children: [\n              Row(\n                mainAxisAlignment: MainAxisAlignment.start,\n                children: [\n                  Container(width: 100, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)),\n                  SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)),\n                  const VerticalDivider(),\n                  const Expanded(child: Text(\"URL\")),\n                ],\n              ),\n              const Divider(thickness: 0.5),\n              Column(children: rows(widget.scripts))\n            ]))));\n  }\n\n  Stack globalMenu() {\n    return Stack(children: [\n      Container(\n          height: 50,\n          width: double.infinity,\n          margin: const EdgeInsets.only(top: 10),\n          decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2)))),\n      Positioned(\n          top: 0,\n          left: 0,\n          right: 0,\n          child: Center(\n              child: TextButton(\n                  onPressed: () {},\n                  child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [\n                    TextButton.icon(\n                        onPressed: () {\n                          export(context, selected.toList());\n                          setState(() {\n                            selected.clear();\n                            multiple = false;\n                          });\n                        },\n                        icon: const Icon(Icons.share, size: 18),\n                        label: Text(localizations.export, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () => removeScripts(selected.toList()),\n                        icon: const Icon(Icons.delete, size: 18),\n                        label: Text(localizations.delete, style: const TextStyle(fontSize: 14))),\n                    TextButton.icon(\n                        onPressed: () {\n                          setState(() {\n                            multiple = false;\n                            selected.clear();\n                          });\n                        },\n                        icon: const Icon(Icons.cancel, size: 18),\n                        label: Text(localizations.cancel, style: const TextStyle(fontSize: 14))),\n                  ]))))\n    ]);\n  }\n\n  List<Widget> rows(List<ScriptItem> list) {\n    var primaryColor = Theme.of(context).colorScheme.primary;\n\n    return List.generate(list.length, (index) {\n      final item = list[index];\n      final isRemote = item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty;\n\n      return InkWell(\n          splashColor: primaryColor.withOpacity(0.3),\n          onTap: () async {\n            if (multiple) {\n              setState(() {\n                if (!selected.add(index)) {\n                  selected.remove(index);\n                }\n              });\n              return;\n            }\n            showEdit(index);\n          },\n          onLongPress: () => showMenus(index),\n          child: Container(\n              color: selected.contains(index)\n                  ? primaryColor.withOpacity(0.8)\n                  : index.isEven\n                      ? Colors.grey.withOpacity(0.1)\n                      : null,\n              height: 45,\n              padding: const EdgeInsets.all(5),\n              child: Row(\n                children: [\n                  SizedBox(\n                      width: 100,\n                      child: Row(children: [\n                        Expanded(child: Text(list[index].name ?? '', style: const TextStyle(fontSize: 13))),\n                        if (isRemote)\n                          const Padding(\n                              padding: EdgeInsets.only(left: 6),\n                              child: Text('R', style: TextStyle(fontSize: 11, color: Colors.blue))),\n                      ])),\n                  SizedBox(\n                      width: 50,\n                      child: Transform.scale(\n                          scale: 0.65,\n                          child: SwitchWidget(\n                              value: list[index].enabled,\n                              onChanged: (val) {\n                                list[index].enabled = val;\n                                _refreshScript();\n                              }))),\n                  const SizedBox(width: 10),\n                  Expanded(\n                      child: Text(list[index].urls.join(', ').fixAutoLines(), style: const TextStyle(fontSize: 13))),\n                ],\n              )));\n    });\n  }\n\n  //点击菜单\n  void showMenus(int index) {\n    setState(() {\n      selected.add(index);\n    });\n\n    showModalBottomSheet(\n        context: context,\n        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),\n        enableDrag: true,\n        builder: (context) {\n          return Wrap(\n            alignment: WrapAlignment.center,\n            children: [\n              BottomSheetItem(\n                  text: localizations.multiple,\n                  onPressed: () {\n                    setState(() => multiple = true);\n                  }),\n              const Divider(thickness: 0.5, height: 1),\n              BottomSheetItem(text: localizations.edit, onPressed: () => showEdit(index)),\n              const Divider(thickness: 0.5, height: 1),\n              BottomSheetItem(text: localizations.share, onPressed: () => export(context, [index])),\n              const Divider(thickness: 0.5, height: 1),\n              BottomSheetItem(\n                  text: widget.scripts[index].enabled ? localizations.disabled : localizations.enable,\n                  onPressed: () {\n                    widget.scripts[index].enabled = !widget.scripts[index].enabled;\n                    _refreshScript();\n                  }),\n              const Divider(thickness: 0.5, height: 1),\n              BottomSheetItem(\n                  text: localizations.delete,\n                  onPressed: () async {\n                    await (await ScriptManager.instance).removeScript(index);\n                    _refreshScript(force: true);\n                    if (context.mounted) FlutterToastr.show(localizations.importSuccess, context);\n                  }),\n              Container(color: Theme.of(context).hoverColor, height: 8),\n              TextButton(\n                child: Container(\n                    height: 45,\n                    width: double.infinity,\n                    padding: const EdgeInsets.only(top: 10),\n                    child: Text(localizations.cancel, textAlign: TextAlign.center)),\n                onPressed: () {\n                  Navigator.of(context).pop();\n                },\n              ),\n            ],\n          );\n        }).then((value) {\n      if (multiple) {\n        return;\n      }\n      setState(() {\n        selected.remove(index);\n      });\n    });\n  }\n\n  Future<void> showEdit([int? index]) async {\n    String? script;\n    if (index != null) {\n      var scriptManager = await ScriptManager.instance;\n      var scriptItem = widget.scripts[index];\n      if (scriptItem.remoteUrl == null || scriptItem.remoteUrl?.isEmpty == true) {\n        script = await scriptManager.getScript(scriptItem);\n      }\n    }\n    if (!mounted) {\n      return;\n    }\n\n    Navigator.of(context)\n        .push(MaterialPageRoute(\n            builder: (context) => ScriptEdit(scriptItem: index == null ? null : widget.scripts[index], script: script)))\n        .then((value) {\n      if (value != null) {\n        setState(() {});\n      }\n    });\n  }\n\n  //导出js\n  Future<void> export(BuildContext context, List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    //文件名称\n    String fileName = 'proxypin-scripts.json';\n    var scriptManager = await ScriptManager.instance;\n    List<dynamic> json = [];\n    for (var idx in indexes) {\n      var item = widget.scripts[idx];\n      var map = item.toJson();\n      map.remove(\"scriptPath\");\n      if (item.remoteUrl == null || item.remoteUrl!.trim().isEmpty) {\n        map['script'] = await scriptManager.getScript(item);\n      }\n      json.add(map);\n    }\n\n    RenderBox? box;\n    if (await Platforms.isIpad() && context.mounted) {\n      box = context.findRenderObject() as RenderBox?;\n    }\n\n    final XFile file = XFile.fromData(utf8.encode(jsonEncode(json)), mimeType: 'json');\n    Share.shareXFiles([file], fileNameOverrides: [fileName], sharePositionOrigin: box?.paintBounds);\n  }\n\n  void enableStatus(bool enable) {\n    for (var idx in selected) {\n      widget.scripts[idx].enabled = enable;\n    }\n    setState(() {});\n    _refreshScript();\n  }\n\n  Future<void> removeScripts(List<int> indexes) async {\n    if (indexes.isEmpty) return;\n    showConfirmDialog(context, content: localizations.confirmContent, onConfirm: () async {\n      var scriptManager = await ScriptManager.instance;\n      for (var idx in indexes) {\n        await scriptManager.removeScript(idx);\n      }\n\n      setState(() {\n        selected.clear();\n      });\n      _refreshScript(force: true);\n\n      if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/ssl.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart' show Clipboard, ClipboardData;\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/native/native_method.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/util/cert/cert_data.dart';\nimport 'package:proxypin/network/util/crts.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/storage/local_storage.dart';\nimport 'package:proxypin/storage/shared_preference_keys.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/mobile/menu/drawer.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass MobileSslWidget extends StatefulWidget {\n  final ProxyServer proxyServer;\n\n  const MobileSslWidget({super.key, required this.proxyServer});\n\n  @override\n  State<MobileSslWidget> createState() => _MobileSslState();\n}\n\nclass _MobileSslState extends State<MobileSslWidget> {\n\n  // iOS CA status\n  bool _loading = false;\n  static bool _installed = false;\n  static bool _trusted = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (Platform.isIOS && _trusted != true) {\n      _refreshStatus();\n    }\n  }\n\n  Future<void> _refreshStatus() async {\n    setState(() => _loading = true);\n    try {\n      final caPem = await CertificateManager.certificatePem();\n      if (Platform.isIOS) {\n        final installedByKeychain = await NativeMethod.isCaInstalled(caPem);\n        _trusted = await evaluateChainTrusted(caPem);\n        _installed = installedByKeychain || _trusted;\n\n        logger.d('[HTTPS] iOS CA status: installed=$_installed keychain=$installedByKeychain trusted=$_trusted');\n      }\n    } catch (e, st) {\n      logger.e('[HTTPS] iOS CA status check error', error: e, stackTrace: st);\n      _installed = false;\n      _trusted = false;\n    } finally {\n      if (mounted) setState(() => _loading = false);\n    }\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final localizations = AppLocalizations.of(context)!;\n    final borderColor = Theme.of(context).dividerColor.withValues(alpha: 0.13);\n    final dividerColor = Theme.of(context).dividerColor.withValues(alpha: 0.22);\n\n    Widget section(List<Widget> tiles) => Card(\n          color: Colors.transparent,\n          elevation: 0,\n          shape: RoundedRectangleBorder(side: BorderSide(color: borderColor), borderRadius: BorderRadius.circular(10)),\n          child: Column(children: tiles),\n        );\n\n    return Scaffold(\n        appBar: AppBar(\n          title: Text(localizations.httpsProxy, style: const TextStyle(fontSize: 16)),\n          centerTitle: true,\n        ),\n        body: ListView(padding: const EdgeInsets.all(12), children: [\n          if (Platform.isIOS)\n            (_loading)\n                ? const Padding(padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()))\n                : CertStatusCard(installed: _installed, trusted: _trusted, proxyServer: widget.proxyServer),\n          // SSL toggle and install\n          section([\n            SwitchListTile(\n                hoverColor: Colors.transparent,\n                title: Text(localizations.enabledHttps),\n                value: widget.proxyServer.enableSsl,\n                onChanged: (val) {\n                  widget.proxyServer.enableSsl = val;\n                  CertificateManager.cleanCache();\n                  setState(() {\n                    widget.proxyServer.configuration.flushConfig();\n                  });\n                }),\n            Divider(height: 0, thickness: 0.3, color: dividerColor),\n            ListTile(\n                title: Text(localizations.installRootCa),\n                trailing: const Icon(Icons.keyboard_arrow_right),\n                onTap: () async {\n                  Navigator.push(\n                      context,\n                      MaterialPageRoute(\n                          builder: (_) => Platform.isIOS\n                              ? IosCaInstall(proxyServer: widget.proxyServer)\n                              : const AndroidCaInstall())).whenComplete(() {\n                    if (Platform.isIOS && !_trusted) _refreshStatus();\n                  });\n                }),\n          ]),\n          const SizedBox(height: 12),\n          // Export options\n          section([\n            ListTile(\n                title: Text(localizations.exportCA),\n                onTap: () async {\n                  final file = await CertificateManager.certificateFile();\n                  _exportFile(\"ProxyPinCA.crt\", file: file);\n                }),\n            Divider(height: 0, thickness: 0.3, color: dividerColor),\n            ListTile(title: Text(localizations.exportCaP12), onTap: exportP12),\n            Divider(height: 0, thickness: 0.3, color: dividerColor),\n            ListTile(\n                title: Text(localizations.exportPrivateKey),\n                onTap: () async {\n                  final file = await CertificateManager.privateKeyFile();\n                  _exportFile(\"ProxyPinKey.pem\", file: file);\n                }),\n          ]),\n          const SizedBox(height: 12),\n          // Import and generate/reset\n          section([\n            ListTile(title: Text(localizations.importCaP12), onTap: importPk12),\n            Divider(height: 0, thickness: 0.3, color: dividerColor),\n            ListTile(\n                title: Text(localizations.generateCA),\n                onTap: () async {\n                  showConfirmDialog(context, title: localizations.generateCA, content: localizations.generateCADescribe,\n                      onConfirm: () async {\n                    await CertificateManager.generateNewRootCA();\n                    if (mounted) FlutterToastr.show(localizations.success, context);\n                    if (Platform.isIOS) _refreshStatus();\n                  });\n                }),\n            Divider(height: 0, thickness: 0.3, color: dividerColor),\n            ListTile(\n                title: Text(localizations.resetDefaultCA),\n                onTap: () async {\n                  showConfirmDialog(context,\n                      title: localizations.resetDefaultCA,\n                      content: localizations.resetDefaultCADescribe, onConfirm: () async {\n                    await CertificateManager.resetDefaultRootCA();\n                    if (mounted) FlutterToastr.show(localizations.success, context);\n                    if (Platform.isIOS) _refreshStatus();\n                  });\n                }),\n          ]),\n        ]));\n  }\n\n  void importPk12() async {\n    FilePickerResult? result =\n        await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['p12', 'pfx']);\n    if (result == null || !mounted) return;\n    //entry password\n    showDialog(\n        context: context,\n        builder: (BuildContext context) {\n          String? password;\n          return SimpleDialog(title: Text(localizations.importCaP12, style: const TextStyle(fontSize: 16)), children: [\n            Padding(\n              padding: const EdgeInsets.all(10),\n              child: TextField(\n                decoration: const InputDecoration(\n                  hintText: \"Enter the password of the p12 file\",\n                  border: OutlineInputBorder(),\n                ),\n                onChanged: (val) => password = val,\n              ),\n            ),\n            Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n              TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n              TextButton(\n                onPressed: () async {\n                  var bytes = await result.files.single.xFile.readAsBytes();\n                  try {\n                    await CertificateManager.importPkcs12(bytes, password);\n                    if (context.mounted) {\n                      FlutterToastr.show(localizations.success, context);\n                      Navigator.pop(context);\n                    }\n                  } catch (e, stackTrace) {\n                    logger.e('import p12 error [$password]', error: e, stackTrace: stackTrace);\n                    if (context.mounted) FlutterToastr.show(localizations.importFailed, context);\n                    return;\n                  }\n                },\n                child: Text(localizations.import),\n              )\n            ])\n          ]);\n        });\n  }\n\n  void exportP12() async {\n    showDialog(\n        context: context,\n        builder: (BuildContext context) {\n          String? password;\n          return SimpleDialog(title: Text(localizations.exportCaP12, style: const TextStyle(fontSize: 16)), children: [\n            Padding(\n              padding: const EdgeInsets.all(10),\n              child: TextField(\n                decoration: const InputDecoration(\n                  hintStyle: TextStyle(color: Colors.grey),\n                  hintText: \"Enter a password to protect p12 file\",\n                  border: OutlineInputBorder(),\n                ),\n                onChanged: (val) => password = val,\n              ),\n            ),\n            Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n              TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),\n              TextButton(\n                onPressed: () async {\n                  var p12Bytes =\n                      await CertificateManager.generatePkcs12(password?.isNotEmpty == true ? password : null);\n                  _exportFile(\"ProxyPinPkcs12.p12\", bytes: p12Bytes);\n\n                  if (context.mounted) Navigator.pop(context);\n                },\n                child: Text(localizations.export),\n              )\n            ])\n          ]);\n        });\n  }\n\n  void _exportFile(String name, {File? file, Uint8List? bytes}) async {\n    bytes ??= await file!.readAsBytes();\n\n    String? outputFile = await FilePicker.platform\n        .saveFile(dialogTitle: 'Please select the path to save:', fileName: name, bytes: bytes);\n\n    if (outputFile != null && mounted) {\n      AppLocalizations localizations = AppLocalizations.of(context)!;\n      FlutterToastr.show(localizations.success, context);\n    }\n  }\n}\n\nclass AndroidCaInstall extends StatefulWidget {\n  const AndroidCaInstall({super.key});\n\n  @override\n  State<StatefulWidget> createState() => _AndroidCaInstallState();\n}\n\nclass _AndroidCaInstallState extends State<AndroidCaInstall> with SingleTickerProviderStateMixin {\n  late TabController _tabController;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    _tabController = TabController(length: 2, vsync: this);\n  }\n\n  @override\n  void dispose() {\n    _tabController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n            centerTitle: true,\n            title: Text(localizations.installRootCa, style: const TextStyle(fontSize: 16)),\n            bottom: TabBar(\n                controller: _tabController,\n                labelPadding: const EdgeInsets.symmetric(horizontal: 5),\n                tabs: <Widget>[\n                  Tab(text: localizations.androidRoot),\n                  Tab(text: localizations.androidUserCA),\n                ])),\n        body: TabBarView(controller: _tabController, children: [rootCA(), userCA()]));\n  }\n\n  ListView rootCA() {\n    bool isCN = localizations.localeName == 'zh';\n    return ListView(padding: const EdgeInsets.all(10), children: [\n      Text(localizations.androidRootMagisk),\n      TextButton(\n          child: Text(\"https://${isCN ? 'gitee' : 'github'}.com/wanghongenpin/Magisk-ProxyPinCA/releases\"),\n          onPressed: () {\n            launchUrl(Uri.parse(\"https://${isCN ? 'gitee' : 'github'}.com/wanghongenpin/Magisk-ProxyPinCA/releases\"));\n          }),\n      const SizedBox(height: 15),\n      futureWidget(\n          CertificateManager.systemCertificateName(),\n          (name) => SelectableText(localizations.androidRootRename(name),\n              style: const TextStyle(fontWeight: FontWeight.w500))),\n      const SizedBox(height: 10),\n      FilledButton(\n          onPressed: () async => _downloadCert(await CertificateManager.systemCertificateName()),\n          child: Text(localizations.androidRootCADownload)),\n      const SizedBox(height: 10),\n      Text(\n        isCN ? \"自动安装（需Root和system写权限，重启生效）\" : \"Auto install (Root & /system write, reboot required)\",\n        style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),\n      ),\n      FilledButton(\n        onPressed: _autoInstallCert,\n        child: Text(isCN ? \"一键自动安装到系统\" : \"Auto install to system\"),\n      ),\n      const SizedBox(height: 10),\n      Text(\n          \"Android 13: ${isCN ? \"将证书挂载到\" : \"Mount the certificate to\"} '/system/etc/security/cacerts' ${isCN ? \"目录\" : \"Directory\"}\"\n              .fixAutoLines()),\n      const SizedBox(height: 5),\n      Text(\n          \"Android 14: ${isCN ? \"将证书挂载到\" : \"Mount the certificate to\"} '/apex/com.android.conscrypt/cacerts' ${isCN ? \"目录\" : \"Directory\"}\"\n              .fixAutoLines()),\n      const SizedBox(height: 5),\n      ClipRRect(\n          child: Align(\n              alignment: Alignment.topCenter,\n              child: Image.network(\n                scale: 0.5,\n                \"https://foruda.gitee.com/images/1710181660282752846/cb520c0b_1073801.png\",\n                height: 460,\n              )))\n    ]);\n  }\n\n  ListView userCA() {\n    bool isCN = localizations.localeName == 'zh';\n\n    return ListView(padding: const EdgeInsets.all(10), children: [\n      Text(localizations.androidUserCATips, style: const TextStyle(fontWeight: FontWeight.w500)),\n      const SizedBox(height: 5),\n      TextButton(\n        style: const ButtonStyle(alignment: Alignment.centerLeft),\n        onPressed: () {},\n        child: Text(\"1. ${localizations.downloadRootCa} \", textAlign: TextAlign.left),\n      ),\n      FilledButton(onPressed: () => _downloadCert('ProxyPinCA.crt'), child: Text(localizations.downloadRootCa)),\n      const SizedBox(height: 5),\n      TextButton(onPressed: () {}, child: Text(\"2. ${localizations.androidUserCAInstall}\")),\n      TextButton(\n          onPressed: () {\n            launchUrl(Uri.parse(isCN\n                ? \"https://gitee.com/wanghongenpin/proxypin/wikis/%E5%AE%89%E5%8D%93%E6%97%A0ROOT%E4%BD%BF%E7%94%A8Xposed%E6%A8%A1%E5%9D%97%E6%8A%93%E5%8C%85\"\n                : \"https://github.com/wanghongenpin/proxypin/wiki/Android-without-ROOT-uses-Xposed-module-to-capture-packets\"));\n          },\n          child: Text(localizations.androidUserXposed)),\n      ClipRRect(\n          child: Align(\n              alignment: Alignment.topCenter,\n              heightFactor: .7,\n              child: Image.network(\n                \"https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png\",\n                height: 680,\n              )))\n    ]);\n  }\n\n  void _downloadCert(String name) async {\n    var caFile = await CertificateManager.certificateFile();\n    String? outputFile = await FilePicker.platform\n        .saveFile(dialogTitle: 'Please select the path to save:', fileName: name, bytes: await caFile.readAsBytes());\n\n    if (outputFile != null && mounted) {\n      AppLocalizations localizations = AppLocalizations.of(context)!;\n      FlutterToastr.show(localizations.success, context);\n    }\n  }\n\n  Future<void> _autoInstallCert() async {\n    bool isEN = localizations.localeName == 'en';\n\n    try {\n      final caFile = await CertificateManager.certificateFile();\n      final hash = await CertificateManager.systemCertificateName();\n      String? destPath;\n      final androidVersion = int.tryParse((await _getAndroidVersion()) ?? \"\");\n      if (androidVersion != null && androidVersion >= 14) {\n        destPath = '/apex/com.android.conscrypt/cacerts/$hash';\n      } else {\n        destPath = '/system/etc/security/cacerts/$hash';\n      }\n      final caPath = caFile.path;\n      final shellCmd = 'cp $caPath $destPath && chmod 644 $destPath && chown root:root $destPath';\n      final result = await Process.run('su', ['-c', shellCmd]);\n      logger.d('Auto install cert result: ${result.stdout}, ${result.stderr}');\n      if (!mounted) return;\n      if (result.exitCode != 0) {\n        FlutterToastr.show(\n            isEN\n                ? 'Certificate install failed. Please check root and /system write permission, or use Magisk module.'\n                : '证书安装失败，请确认Root权限和system写权限，或参考Magisk模块安装。',\n            context,\n            rootNavigator: true,\n            duration: 5);\n        return;\n      }\n      FlutterToastr.show(\n        isEN ? 'Certificate installed, reboot required' : '证书已安装，重启手机后生效',\n        context,\n        rootNavigator: true,\n        duration: 5,\n      );\n    } catch (e) {\n      logger.d('auto install cert error：$e');\n      FlutterToastr.show(\n          isEN\n              ? 'Auto install failed: $e. Please check root and /system write permission, or use Magisk module.'\n              : '自动安装失败：$e，请确认Root和system写权限，或参考Magisk模块安装。',\n          context,\n          rootNavigator: true,\n          duration: 5);\n    }\n  }\n\n  Future<String?> _getAndroidVersion() async {\n    try {\n      final result = await Process.run('getprop', ['ro.build.version.release']);\n      if (result.exitCode == 0) {\n        return result.stdout.toString().trim().split(\".\")[0];\n      }\n    } catch (e) {\n      logger.d('获取Android版本失败：$e');\n    }\n    return null;\n  }\n}\n\nFuture<bool> evaluateChainTrusted(String caPem) async {\n  const host = 'example.com';\n  final leafPem = await CertificateManager.generateLeafCertificatePem(host);\n  return await NativeMethod.evaluateChainTrusted(leafPem, caPem, host: host);\n}\n\nclass IOSCertChecker {\n  static bool checked = false;\n\n  static void check(BuildContext context) async {\n    if (checked || !Platform.isIOS) {\n      return;\n    }\n    logger.d(\"[IosCertChecker] checking iOS CA status\");\n    checked = true;\n\n    if (ProxyServer.current?.enableSsl != true) {\n      return;\n    }\n    if ((await LocalStorage.getBool(SharedPreferenceKeys.CERT_INSTALL_SKIP)) == true) {\n      return;\n    }\n    final caPem = await CertificateManager.certificatePem();\n    bool installed = await NativeMethod.isCaInstalled(caPem);\n    bool trusted = false;\n    if (installed) {\n      trusted = await evaluateChainTrusted(caPem);\n    }\n\n    if ((!installed || !trusted) && context.mounted) {\n      showDialog(\n          context: context,\n          builder: (context) {\n            final localizations = AppLocalizations.of(context)!;\n            return AlertDialog(\n              titlePadding: EdgeInsets.zero,\n              contentPadding: EdgeInsets.zero,\n              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),\n              content: Container(\n                  constraints: const BoxConstraints(maxHeight: 185),\n                  child: CertStatusCard(\n                      installed: installed,\n                      trusted: trusted,\n                      margin: EdgeInsets.zero,\n                      proxyServer: ProxyServer.current!)),\n              actions: <Widget>[\n                TextButton(\n                  onPressed: () {\n                    LocalStorage.setBool(SharedPreferenceKeys.CERT_INSTALL_SKIP, true);\n                    Navigator.pop(context);\n                  },\n                  child: Text(localizations.appUpdateIgnoreBtnTxt),\n                ),\n                TextButton(\n                  onPressed: () => Navigator.pop(context),\n                  child: Text(localizations.cancel),\n                ),\n              ],\n            );\n          });\n    }\n  }\n}\n\nclass CertStatusCard extends StatelessWidget {\n  final bool installed;\n  final bool trusted;\n  final ProxyServer proxyServer;\n  final EdgeInsetsGeometry? margin;\n\n  const CertStatusCard({\n    super.key,\n    required this.installed,\n    required this.trusted,\n    required this.proxyServer,\n    this.margin = const EdgeInsets.all(12),\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n    Color color;\n    IconData icon;\n    String title;\n    String subtitle;\n\n    if (!installed) {\n      color = Colors.red;\n      icon = Icons.error_outline;\n      title = isCN ? '证书未安装' : 'Certificate Not Installed';\n      subtitle = isCN ? '点击“安装根证书”进行安装' : 'Tap \"Install Root CA\" to proceed';\n    } else if (!trusted) {\n      color = Colors.orange;\n      icon = Icons.warning_amber_rounded;\n      title = isCN ? '证书未信任' : 'Certificate Not Trusted';\n      subtitle = AppLocalizations.of(context)!.trustCaDescribe;\n    } else {\n      return SizedBox();\n    }\n\n    return Card(\n      margin: margin,\n      elevation: 2,\n      child: Padding(\n        padding: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 6),\n        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n          Row(children: [\n            Icon(icon, color: color, size: 28),\n            const SizedBox(width: 8),\n            Expanded(child: Text(title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: color))),\n          ]),\n          TextButton(\n              onPressed: () {\n                navigator(context, IosCaInstall(proxyServer: proxyServer));\n              },\n              child: Text(subtitle)),\n        ]),\n      ),\n    );\n  }\n}\n\nclass IosCaInstall extends StatefulWidget {\n  final ProxyServer proxyServer;\n\n  const IosCaInstall({super.key, required this.proxyServer});\n\n  @override\n  State<IosCaInstall> createState() => _IosCaInstallState();\n}\n\nclass _IosCaInstallState extends State<IosCaInstall> {\n  bool loading = true;\n  bool installed = false;\n  bool trusted = false;\n  X509CertificateData? certDetails;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    _refreshStatus();\n  }\n\n  Future<void> _refreshStatus() async {\n    setState(() => loading = true);\n    try {\n      certDetails = CertificateManager.caCert ?? await CertificateManager.getCertificateDetails();\n      final caPem = await CertificateManager.certificatePem();\n      if (Platform.isIOS) {\n        trusted = await evaluateChainTrusted(caPem);\n        // Installation check: best-effort keychain lookup; if chain trusted, consider installed\n        final installedByKeychain = await NativeMethod.isCaInstalled(caPem);\n        installed = installedByKeychain || trusted;\n      } else {\n        installed = false;\n        trusted = false;\n      }\n    } catch (e, st) {\n      logger.e('iOS CA status check error', error: e, stackTrace: st);\n      installed = false;\n      trusted = false;\n    } finally {\n      if (mounted) setState(() => loading = false);\n    }\n  }\n\n  void _downloadCert() async {\n    logger.d('[IosCaInstall] user tapped download cert');\n    CertificateManager.cleanCache();\n    await widget.proxyServer.retryBind();\n    final url = Uri.parse(\"http://127.0.0.1:${widget.proxyServer.port}/ssl\");\n    launchUrl(url, mode: LaunchMode.externalApplication);\n  }\n\n  void _copyProxyLink() async {\n    CertificateManager.cleanCache();\n    await widget.proxyServer.retryBind();\n    var urlStr = Uri.parse(\"http://127.0.0.1:${widget.proxyServer.port}/ssl\").toString();\n    Clipboard.setData(ClipboardData(text: urlStr)).then((_) {\n      if (!mounted) {\n        return;\n      }\n      FlutterToastr.show(localizations.copied, context);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(localizations.installRootCa, style: const TextStyle(fontSize: 16)),\n        actions: [IconButton(onPressed: _refreshStatus, icon: const Icon(Icons.refresh))],\n      ),\n      body: loading\n          ? const Center(child: CircularProgressIndicator())\n          : ListView(padding: const EdgeInsets.all(12), children: [\n              _statusCard(isCN),\n              // const SizedBox(height: 12),\n              // _actionSection(isCN),\n              const SizedBox(height: 24),\n              _guideSection(isCN),\n            ]),\n    );\n  }\n\n  Widget _statusCard(bool isCN) {\n    Color color;\n    IconData icon;\n    String title;\n    String? subtitle;\n\n    if (!installed) {\n      color = Colors.red;\n      icon = Icons.error_outline;\n      title = isCN ? '证书未安装' : 'Certificate Not Installed';\n      subtitle = '${localizations.download} & ${localizations.installCaDescribe}';\n    } else if (!trusted) {\n      color = Colors.orange;\n      icon = Icons.warning_amber_rounded;\n      title = isCN ? '证书未信任' : 'Certificate Not Trusted';\n      subtitle = localizations.trustCaDescribe;\n    } else {\n      color = Colors.green;\n      icon = Icons.verified_rounded;\n      title = isCN ? '证书已安装并信任' : 'Certificate Installed & Trusted';\n    }\n\n    return Card(\n      elevation: 2,\n      child: Padding(\n        padding: const EdgeInsets.all(16.0),\n        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n          Row(children: [\n            Icon(icon, color: color, size: 28),\n            const SizedBox(width: 8),\n            Expanded(child: Text(title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: color))),\n          ]),\n          const SizedBox(height: 8),\n          if (subtitle != null) Text(subtitle),\n          const SizedBox(height: 12),\n          if (!installed) ...[\n            Padding(\n                padding: EdgeInsets.symmetric(horizontal: 24),\n                child: FilledButton.icon(\n                    style: FilledButton.styleFrom(\n                        minimumSize: const Size.fromHeight(40), // Make button full width\n                        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),\n                    onPressed: _downloadCert,\n                    icon: const Icon(Icons.download),\n                    label: Text(localizations.downloadRootCa))),\n            TextButton.icon(\n                onPressed: _copyProxyLink, icon: const Icon(Icons.link), label: Text(localizations.downloadRootCaNote))\n          ],\n          if (trusted && certDetails != null) ...[const Divider(height: 12), _certDetails(certDetails!)]\n        ]),\n      ),\n    );\n  }\n\n  Widget _certDetails(X509CertificateData details) {\n    final infoLabelStyle = Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]);\n    final infoValueStyle = Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500);\n\n    return Column(children: [\n      const SizedBox(height: 6),\n      _kv('Name', details.subject['2.5.4.3'] ?? 'ProxyPin CA', infoLabelStyle, infoValueStyle),\n      const SizedBox(height: 6),\n      _kv('Expires', details.validity.notAfter.toLocal().toString().split(' ').first, infoLabelStyle, infoValueStyle),\n      // const SizedBox(height: 6),\n      // _kv('Fingerprint', details.sha1Thumbprint ?? '-', infoLabelStyle, infoValueStyle),\n    ]);\n  }\n\n  Widget _kv(String k, String v, TextStyle? ks, TextStyle? vs) {\n    return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [\n      Text(k, style: ks),\n      const Spacer(),\n      Expanded(\n          child: SelectableText(\n        v,\n        style: vs,\n        textAlign: TextAlign.right,\n        maxLines: 2,\n        minLines: 1,\n      ))\n    ]);\n  }\n\n  Widget _guideSection(bool isCN) {\n    return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n      Text(isCN ? '指引' : 'Guide', style: const TextStyle(fontWeight: FontWeight.w600)),\n      const SizedBox(height: 6),\n      TextButton(onPressed: () => _downloadCert(), child: Text(\"1. ${localizations.downloadRootCa}\")),\n      TextButton(onPressed: _copyProxyLink, child: Text(localizations.downloadRootCaNote)),\n      TextButton(onPressed: () {}, child: Text(\"2. ${localizations.installRootCa} -> ${localizations.trustCa}\")),\n      TextButton(onPressed: () {}, child: Text(\"2.1 ${localizations.installCaDescribe}\")),\n      Padding(\n          padding: const EdgeInsets.only(left: 15),\n          child:\n              Image.network(\"https://foruda.gitee.com/images/1689346516243774963/c56bc546_1073801.png\", height: 400)),\n      TextButton(onPressed: () {}, child: Text(\"2.2 ${localizations.trustCaDescribe}\")),\n      Padding(\n          padding: const EdgeInsets.only(left: 15),\n          child:\n              Image.network(\"https://foruda.gitee.com/images/1689346614916658100/fd9b9e41_1073801.png\", height: 270)),\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/theme.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/ui/configuration.dart';\n\nclass MobileThemeSetting extends StatelessWidget {\n  final AppConfiguration appConfiguration;\n\n  const MobileThemeSetting({super.key, required this.appConfiguration});\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    return PopupMenuButton(\n        tooltip: appConfiguration.themeMode.name,\n        surfaceTintColor: Theme.of(context).colorScheme.onPrimary,\n        offset: const Offset(150, 0),\n        itemBuilder: (BuildContext context) {\n          return [\n            PopupMenuItem(\n                child: Tooltip(\n                    preferBelow: false,\n                    message: localizations.material3,\n                    child: SwitchListTile(\n                      value: appConfiguration.useMaterial3,\n                      onChanged: (bool value) {\n                        appConfiguration.useMaterial3 = value;\n                        Navigator.of(context).pop();\n                      },\n                      dense: true,\n                      title: const Text(\"Material3\"),\n                    ))),\n            PopupMenuItem(\n                child:\n                    ListTile(trailing: const Icon(Icons.cached), dense: true, title: Text(localizations.followSystem)),\n                onTap: () => appConfiguration.themeMode = ThemeMode.system),\n            PopupMenuItem(\n                child: ListTile(trailing: const Icon(Icons.sunny), dense: true, title: Text(localizations.themeLight)),\n                onTap: () => appConfiguration.themeMode = ThemeMode.light),\n            PopupMenuItem(\n                child: ListTile(\n                    trailing: const Icon(Icons.nightlight_outlined), dense: true, title: Text(localizations.themeDark)),\n                onTap: () => appConfiguration.themeMode = ThemeMode.dark),\n          ];\n        },\n        child: ListTile(\n          title: Text(localizations.theme),\n          trailing: getIcon(),\n        ));\n  }\n\n  Icon getIcon() {\n    switch (appConfiguration.themeMode) {\n      case ThemeMode.system:\n        return const Icon(Icons.cached);\n      case ThemeMode.dark:\n        return const Icon(Icons.nightlight_outlined);\n      case ThemeMode.light:\n        return const Icon(Icons.sunny);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/setting/video_player.dart",
    "content": "// import 'dart:async';\n//\n// import 'package:flutter/material.dart';\n// import 'package:video_player/video_player.dart';\n//\n// void main() => runApp(const VideoPlayerApp());\n//\n// class VideoPlayerApp extends StatelessWidget {\n//   const VideoPlayerApp({super.key});\n//\n//   @override\n//   Widget build(BuildContext context) {\n//     return const MaterialApp(\n//       title: 'Video Player Demo',\n//       home: VideoPlayerScreen(),\n//     );\n//   }\n// }\n//\n// class VideoPlayerScreen extends StatefulWidget {\n//   const VideoPlayerScreen({super.key});\n//\n//   @override\n//   State<VideoPlayerScreen> createState() => _VideoPlayerScreenState();\n// }\n//\n// class _VideoPlayerScreenState extends State<VideoPlayerScreen> {\n//   late VideoPlayerController _controller;\n//   late Future<void> _initializeVideoPlayerFuture;\n//\n//   @override\n//   void initState() {\n//     super.initState();\n//\n//     _controller = VideoPlayerController.network(\n//       'https://github.com/wanghongenpin/proxypin/assets/24794200/38bc5a83-999f-4af2-9d74-863532a81cef',\n//       videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true, allowBackgroundPlayback: true),\n//     );\n//     _initializeVideoPlayerFuture = _controller.initialize();\n//     _initializeVideoPlayerFuture.whenComplete(() {\n//       final MediaQueryData data = MediaQuery.of(context);\n//\n//       EdgeInsets paddingSafeArea = data.padding;\n//       double widthScreen = data.size.width;\n//       _controller.setPictureInPictureOverlayRect(\n//           rect: Rect.fromLTWH(0, paddingSafeArea.top, widthScreen, 9 * widthScreen / 16));\n//       // _controller.setAutomaticallyStartPictureInPicture(enableStartPictureInPictureAutomaticallyFromInline: true);\n//     });\n//     _controller.addListener(() {\n//       setState(() {});\n//     });\n//   }\n//\n//   @override\n//   void dispose() {\n//     // Ensure disposing of the VideoPlayerController to free up resources.\n//     _controller.dispose();\n//     super.dispose();\n//   }\n//\n//   @override\n//   Widget build(BuildContext context) {\n//     return FutureBuilder(\n//       future: _initializeVideoPlayerFuture,\n//       builder: (context, snapshot) {\n//         if (snapshot.connectionState == ConnectionState.done) {\n//           return AspectRatio(\n//             aspectRatio: _controller.value.aspectRatio,\n//             // Use the VideoPlayer widget to display the video.\n//             child: Stack(alignment: Alignment.bottomCenter, children: [\n//               VideoPlayer(_controller),\n//               _ControlsOverlay(controller: _controller),\n//               VideoProgressIndicator(\n//                 _controller,\n//                 allowScrubbing: true,\n//               ),\n//             ]),\n//           );\n//         } else {\n//           // If the VideoPlayerController is still initializing, show a\n//           // loading spinner.\n//           return const Center(\n//             child: CircularProgressIndicator(),\n//           );\n//         }\n//       },\n//     );\n//   }\n// }\n//\n// class _ControlsOverlay extends StatelessWidget {\n//   const _ControlsOverlay({required this.controller});\n//\n//   static const List<double> _playbackRates = <double>[\n//     0.25,\n//     0.5,\n//     1.0,\n//     1.5,\n//     2.0,\n//   ];\n//\n//   final VideoPlayerController controller;\n//\n//   @override\n//   Widget build(BuildContext context) {\n//     return Stack(\n//       children: <Widget>[\n//         AnimatedSwitcher(\n//           duration: const Duration(milliseconds: 50),\n//           reverseDuration: const Duration(milliseconds: 200),\n//           child: controller.value.isPlaying\n//               ? const SizedBox.shrink()\n//               : Container(\n//                   color: Colors.black26,\n//                   child: const Center(\n//                     child: Icon(\n//                       Icons.play_arrow,\n//                       color: Colors.white,\n//                       size: 80.0,\n//                       semanticLabel: 'Play',\n//                     ),\n//                   ),\n//                 ),\n//         ),\n//         GestureDetector(\n//           onTap: () {\n//             controller.value.isPlaying ? controller.pause() : controller.play();\n//           },\n//         ),\n//         Align(\n//             alignment: Alignment.topRight,\n//             child: IconButton(\n//               onPressed: () async {\n//                 controller.startPictureInPicture();\n//               },\n//               icon: const Icon(Icons.picture_in_picture),\n//             )),\n//         Align(\n//           alignment: Alignment.bottomRight,\n//           child: PopupMenuButton<double>(\n//             initialValue: controller.value.playbackSpeed,\n//             tooltip: 'Playback speed',\n//             onSelected: (double speed) {\n//               controller.setPlaybackSpeed(speed);\n//             },\n//             itemBuilder: (BuildContext context) {\n//               return <PopupMenuItem<double>>[\n//                 for (final double speed in _playbackRates)\n//                   PopupMenuItem<double>(\n//                     value: speed,\n//                     child: Text('${speed}x'),\n//                   )\n//               ];\n//             },\n//             child: Padding(\n//               padding: const EdgeInsets.symmetric(\n//                 vertical: 15,\n//                 horizontal: 16,\n//               ),\n//               child: Text('${controller.value.playbackSpeed}x', style: const TextStyle(color: Colors.white)),\n//             ),\n//           ),\n//         ),\n//       ],\n//     );\n//   }\n// }\n"
  },
  {
    "path": "lib/ui/mobile/widgets/about.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:url_launcher/url_launcher.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\n\nimport '../../app_update/app_update_repository.dart';\n\n/// 关于\nclass About extends StatefulWidget {\n  const About({super.key});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _AboutState();\n  }\n}\n\nclass _AboutState extends State<About> {\n  bool checkUpdating = false;\n\n  @override\n  Widget build(BuildContext context) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    String gitHub = \"https://github.com/wanghongenpin/proxypin\";\n    final String sponsorUrl = \"https://github.com/sponsors/wanghongenpin\";\n\n    return Scaffold(\n        appBar: AppBar(title: Text(localizations.about, style: const TextStyle(fontSize: 16)), centerTitle: true),\n        body: ListView(padding: const EdgeInsets.all(12), children: [\n          const SizedBox(height: 6),\n          Center(child: Text(\"ProxyPin\", style: Theme.of(context).textTheme.headlineSmall)),\n          const SizedBox(height: 10),\n          Center(\n              child: Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 10),\n                  child: Text(localizations.proxyPinSoftware, textAlign: TextAlign.center))),\n          const SizedBox(height: 8),\n          Center(child: Text(\"Version ${AppConfiguration.version}\")),\n          const SizedBox(height: 12),\n          Card(\n              color: Colors.transparent,\n              elevation: 0,\n              shape: RoundedRectangleBorder(\n                  side: BorderSide(color: Theme.of(context).dividerColor.withValues(alpha: 0.13)),\n                  borderRadius: BorderRadius.circular(10)),\n              child: Column(children: [\n                ListTile(\n                    title: const Text(\"GitHub\"),\n                    trailing: const Icon(Icons.open_in_new, size: 22),\n                    onTap: () {\n                      _safeLaunch(Uri.parse(gitHub));\n                    }),\n                Divider(height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                ListTile(\n                    title: Text(localizations.feedback),\n                    trailing: const Icon(Icons.open_in_new, size: 22),\n                    onTap: () {\n                      _safeLaunch(Uri.parse(\"$gitHub/issues\"));\n                    }),\n                Divider(height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                ListTile(\n                    title: Text(localizations.appUpdateCheckVersion),\n                    trailing: checkUpdating\n                        ? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2))\n                        : const Icon(Icons.sync, size: 22),\n                    onTap: () async {\n                      if (checkUpdating) return;\n                      setState(() => checkUpdating = true);\n                      await AppUpdateRepository.checkUpdate(context, canIgnore: false, showToast: true);\n                      if (mounted) setState(() => checkUpdating = false);\n                    }),\n                Divider(height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                ListTile(\n                    title: Text(localizations.download),\n                    trailing: const Icon(Icons.open_in_new, size: 22),\n                    onTap: () {\n                      final url = \"$gitHub/releases\";\n                      _safeLaunch(Uri.parse(url));\n                    }),\n                Divider(height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                ListTile(\n                    title: Text(localizations.privacyPolicy),\n                    trailing: const Icon(Icons.privacy_tip_outlined, size: 22),\n                    onTap: () {\n                      showDialog(\n                        context: context,\n                        builder: (ctx) => AlertDialog(\n                          title: Text(localizations.privacyPolicy),\n                          content: SingleChildScrollView(\n                              child: ConstrainedBox(\n                                  constraints: const BoxConstraints(maxWidth: 385),\n                                  child: Text(localizations.privacyContent, style: const TextStyle(height: 1.35)))),\n                          actions: [\n                            TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(localizations.close))\n                          ],\n                        ),\n                      );\n                    }),\n                Divider(height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),\n                // Sponsor / Donate entry\n                ListTile(\n                  title: Text(localizations.sponsorDonate),\n                  subtitle: Text(localizations.sponsorSupport, style: const TextStyle(fontSize: 12)),\n                  trailing: const Icon(Icons.favorite, color: Colors.redAccent, size: 22),\n                  onTap: () => _showSponsorDialog(localizations, sponsorUrl),\n                ),\n              ]))\n        ]));\n  }\n\n  Future<void> _safeLaunch(Uri uri) async {\n    await launchUrl(uri, mode: LaunchMode.externalApplication);\n  }\n\n  void _showSponsorDialog(AppLocalizations l10n, String sponsorUrl) {\n    bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n    List<Widget> sponsors = [\n      ListTile(\n        onTap: () => _safeLaunch(Uri.parse(\"https://afdian.com/a/proxypin\")),\n        contentPadding: EdgeInsets.zero,\n        leading: const Icon(Icons.favorite, color: Colors.pinkAccent),\n        title: Text(l10n.sponsorAfdian),\n      )\n    ];\n\n    final coffee = ListTile(\n      contentPadding: EdgeInsets.zero,\n      leading: const Icon(Icons.coffee, color: Colors.brown),\n      title: Text('Buy Me a Coffee'),\n      onTap: () => _safeLaunch(Uri.parse(\"https://buymeacoffee.com/proxypin\")),\n    );\n    if (isCN) {\n      sponsors.add(coffee);\n    } else {\n      sponsors.insert(0, coffee);\n    }\n\n    showDialog(\n      context: context,\n      builder: (ctx) {\n        return AlertDialog(\n          title: Text(l10n.sponsorDonate),\n          contentPadding: const EdgeInsets.only(left: 20, top: 10, right: 20, bottom: 10),\n          content: SizedBox(\n            width: 340,\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [Text(l10n.sponsorThanks), const SizedBox(height: 12), ...sponsors],\n            ),\n          ),\n          actions: [\n            TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(l10n.close)),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/widgets/floating_window.dart",
    "content": "import 'package:flutter/material.dart';\n\n///悬浮小窗口\nclass FloatingWindowManager {\n  static final FloatingWindowManager _instance = FloatingWindowManager._();\n\n  factory FloatingWindowManager() => _instance;\n\n  FloatingWindowManager._();\n\n  ///浮窗\n  OverlayEntry? overlayEntry;\n\n  bool get isShow => overlayEntry != null;\n\n  void show(BuildContext context, {required Widget widget}) {\n    if (overlayEntry == null) {\n      // var floatingWindow = FloatingWindow(top: 160, left: 210, child: Material(child: child));\n      overlayEntry = OverlayEntry(builder: (BuildContext context) {\n        return widget;\n      });\n      Overlay.of(context).insert(overlayEntry!);\n    }\n  }\n\n  ///关闭小窗\n  void hide() {\n    overlayEntry?.remove();\n    overlayEntry = null;\n  }\n}\n\nclass FloatingWindow extends StatefulWidget {\n  final Widget child;\n  final double top;\n  final double right;\n\n  const FloatingWindow({\n    super.key,\n    required this.child,\n    required this.top,\n    required this.right,\n  });\n\n  @override\n  State<FloatingWindow> createState() => _FloatingWindowState();\n}\n\nclass _FloatingWindowState extends State<FloatingWindow> with TickerProviderStateMixin {\n  double right = 0;\n  double top = 0;\n\n  double maxX = 0;\n  double maxY = 0;\n\n  var parentKey = GlobalKey();\n  var childKey = GlobalKey();\n\n  var parentSize = const Size(0, 0);\n  var childSize = const Size(0, 0);\n\n  void changeState() {\n    setState(() {});\n  }\n\n  @override\n  void initState() {\n    right = widget.right;\n    top = widget.top;\n    WidgetsBinding.instance.addPostFrameCallback((d) {\n      parentSize = getWidgetSize(parentKey);\n      childSize = getWidgetSize(childKey);\n      maxX = parentSize.width - childSize.width;\n      maxY = parentSize.height - childSize.height;\n    });\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      key: parentKey,\n      fit: StackFit.expand,\n      children: [\n        Positioned(\n          key: childKey,\n          right: right,\n          top: top,\n          child: GestureDetector(\n            onPanUpdate: (d) {\n              var delta = d.delta;\n              right -= delta.dx;\n              top += delta.dy;\n              setState(() {});\n            },\n            onPanEnd: (d) {\n              right = getValue(right, maxX);\n              top = getValue(top, maxY);\n            },\n            child: widget.child,\n          ),\n        )\n      ],\n    );\n  }\n\n  ///限制边界\n  double getValue(double value, double max) {\n    if (value < 0) {\n      return 0;\n    } else if (value > max) {\n      return max;\n    } else {\n      return value;\n    }\n  }\n\n  Size getWidgetSize(GlobalKey key) {\n    final RenderBox renderBox = key.currentContext?.findRenderObject() as RenderBox;\n    return renderBox.size;\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/widgets/highlight.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang All rights reserved.\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\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/ui/component/state_component.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/utils/keyword_highlight.dart';\n\nclass KeywordHighlight extends StatefulWidget {\n  const KeywordHighlight({super.key});\n\n  @override\n  State<KeywordHighlight> createState() => _KeywordHighlightState();\n}\n\nclass _KeywordHighlightState extends State<KeywordHighlight> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    var colors = {\n      Colors.red: localizations.red,\n      Colors.yellow.shade600: localizations.yellow,\n      Colors.blue: localizations.blue,\n      Colors.green: localizations.green,\n      Colors.grey: localizations.gray,\n    };\n\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(localizations.keyword + localizations.highlight,\n            style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n        actions: [\n          SwitchWidget(\n              scale: 0.7, value: KeywordHighlights.enabled, onChanged: (val) => KeywordHighlights.enabled = val),\n          const SizedBox(width: 10)\n        ],\n      ),\n      body: DefaultTabController(\n        length: colors.length,\n        child: Scaffold(\n          appBar: TabBar(tabs: colors.entries.map((e) => Tab(text: e.value)).toList()),\n          body: TabBarView(\n              children: colors.entries\n                  .map((e) => KeepAliveWrapper(\n                      child: Padding(\n                          padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 15),\n                          child: TextFormField(\n                            minLines: 2,\n                            maxLines: 2,\n                            initialValue: KeywordHighlights.keywords[e.key],\n                            onChanged: (value) {\n                              if (value.isEmpty) {\n                                KeywordHighlights.keywords.remove(e.key);\n                              } else {\n                                KeywordHighlights.keywords[e.key] = value;\n                              }\n                            },\n                            decoration: decoration(localizations.keyword),\n                          ))))\n                  .toList()),\n        ),\n      ),\n    );\n  }\n\n  InputDecoration decoration(String label, {String? hintText}) {\n    return InputDecoration(\n      floatingLabelBehavior: FloatingLabelBehavior.always,\n      labelText: label,\n      isDense: true,\n      border: const OutlineInputBorder(),\n    );\n  }\n\n  @override\n  void dispose() {\n    if (KeywordHighlights.enabled) {\n      KeywordHighlights.saveKeywords(Map.from(KeywordHighlights.keywords));\n    } else {\n      KeywordHighlights.saveKeywords(KeywordHighlights.keywords);\n    }\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/widgets/pip.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/native/pip.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/utils/ip.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/listenable_list.dart';\n\n/// Picture in Picture Window\nclass PictureInPictureWindow extends StatefulWidget {\n  final ListenableList<HttpRequest> container;\n\n  const PictureInPictureWindow(this.container, {super.key});\n\n  @override\n  State<PictureInPictureWindow> createState() => _PictureInPictureWindowState();\n}\n\nclass _PictureInPictureWindowState extends State<PictureInPictureWindow> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  OnchangeListEvent<HttpRequest>? changeEvent;\n\n  @override\n  void initState() {\n    super.initState();\n    changeEvent = OnchangeListEvent(() {\n      setState(() {});\n    });\n    widget.container.addListener(changeEvent!);\n  }\n\n  @override\n  void dispose() {\n    widget.container.removeListener(changeEvent!);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.container.isEmpty) {\n      return Material(child: Center(child: Text(localizations.emptyData, style: const TextStyle(color: Colors.grey))));\n    }\n\n    return Material(\n        child: ListView.separated(\n            padding: const EdgeInsets.only(left: 2),\n            itemCount: widget.container.length,\n            separatorBuilder: (context, index) => const Divider(thickness: 0.3, height: 0.5),\n            itemBuilder: (context, index) {\n              return Text.rich(\n                  overflow: TextOverflow.ellipsis,\n                  TextSpan(\n                      text: widget.container.elementAt(widget.container.length - index - 1).requestUrl.fixAutoLines(),\n                      style: const TextStyle(fontSize: 9)),\n                  maxLines: 2);\n            }));\n  }\n}\n\n/// pip Icon\nclass PictureInPictureIcon extends StatefulWidget {\n  final ProxyServer proxyServer;\n\n  const PictureInPictureIcon(\n    this.proxyServer, {\n    super.key,\n  });\n\n  @override\n  State<PictureInPictureIcon> createState() => _PictureInPictureState();\n}\n\nclass _PictureInPictureState extends State<PictureInPictureIcon> {\n  static double xPosition = -1;\n  static double yPosition = -1;\n  static Size? size;\n  late final double _top;\n  late final double _bottom;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n\n    AppConfiguration.current?.pipIcon.addListener(() {\n      setState(() {});\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (AppConfiguration.current?.pipIcon.value != true) return const SizedBox();\n\n    size ??= MediaQuery.sizeOf(context);\n    if (size == null || size!.isEmpty) {\n      size = null;\n      return const SizedBox();\n    }\n\n    if (xPosition == -1) {\n      xPosition = size!.width - 48;\n      yPosition = size!.height * 0.35;\n      _top = MediaQuery.of(context).padding.top;\n      _bottom = size!.height - 48 - (AppConfiguration.current?.bottomNavigation == false ? 0 : 56);\n    }\n\n    return Positioned(\n      top: yPosition,\n      left: xPosition,\n      child: GestureDetector(\n          onPanUpdate: (tapInfo) {\n            // if (xPosition + tapInfo.delta.dx < 0) return;\n            // if (yPosition + tapInfo.delta.dy < 0) return;\n\n            setState(() {\n              xPosition = (xPosition + tapInfo.delta.dx).clamp(0, size!.width);\n              yPosition = (yPosition + tapInfo.delta.dy).clamp(_top, _bottom);\n            });\n          },\n          child: IconButton(\n              tooltip: localizations.windowMode,\n              onPressed: () async {\n                var configuration = widget.proxyServer.configuration;\n                List<String>? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : [];\n                List<String>? disallowApps;\n                if (appList.isEmpty) {\n                  disallowApps = configuration.appBlacklist ?? [];\n                }\n\n                PictureInPicture.enterPictureInPictureMode(\n                    Platform.isAndroid ? await localIp() : \"127.0.0.1\", widget.proxyServer.port,\n                    appList: appList, disallowApps: disallowApps);\n              },\n              icon: const Icon(Icons.picture_in_picture_alt))),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/mobile/widgets/remote_device.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/native/vpn.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/network/components/host_filter.dart';\nimport 'package:proxypin/network/components/manager/request_rewrite_manager.dart';\nimport 'package:proxypin/network/components/manager/script_manager.dart';\nimport 'package:proxypin/network/http/http_client.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/app_dialog.dart';\nimport 'package:proxypin/ui/component/qrcode/qr_scan_view.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/component/widgets.dart';\nimport 'package:proxypin/utils/ip.dart';\nimport 'package:qr_flutter/qr_flutter.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\n///远程设备\n///Remote device\n///@author Hongen Wang\nclass RemoteModel {\n  final bool connect;\n  final String? host;\n  final int? port;\n  final String? os;\n  final String? hostname;\n  final bool? ipProxy;\n\n  RemoteModel({\n    required this.connect,\n    this.host,\n    this.port,\n    this.os,\n    this.hostname,\n    this.ipProxy,\n  });\n\n  factory RemoteModel.fromJson(Map<String, dynamic> json) {\n    return RemoteModel(\n        connect: json['connect'],\n        host: json['host'],\n        port: json['port'],\n        os: json['os'],\n        hostname: json['hostname'],\n        ipProxy: json['ipProxy'] == true);\n  }\n\n  RemoteModel copyWith({\n    bool? connect,\n    String? host,\n    int? port,\n    String? os,\n    String? hostname,\n    bool? ipProxy,\n  }) {\n    return RemoteModel(\n      connect: connect ?? this.connect,\n      host: host ?? this.host,\n      port: port ?? this.port,\n      os: os ?? this.os,\n      hostname: hostname ?? this.hostname,\n      ipProxy: ipProxy ?? this.ipProxy,\n    );\n  }\n\n  String get identification => '$host:$port';\n\n  //host和端口是否相等\n  bool equals(RemoteModel remoteModel) {\n    return identification == remoteModel.identification;\n  }\n\n  Map<String, dynamic> toJson() {\n    return {'connect': connect, 'host': host, 'port': port, 'os': os, 'hostname': hostname, 'ipProxy': ipProxy};\n  }\n}\n\nclass RemoteDevicePage extends StatefulWidget {\n  final ProxyServer proxyServer;\n  final ValueNotifier<RemoteModel> remoteDevice;\n\n  const RemoteDevicePage({super.key, required this.proxyServer, required this.remoteDevice});\n\n  @override\n  State<RemoteDevicePage> createState() => _RemoteDevicePageState();\n}\n\nclass _RemoteDevicePageState extends State<RemoteDevicePage> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  bool syncConfig = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: true,\n        title: Text(localizations.remoteDevice, style: const TextStyle(fontSize: 16)),\n        actions: [\n          PopupMenuButton(\n            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),\n            elevation: 8,\n            color: Theme.of(context).colorScheme.surface,\n            icon: const Icon(Icons.add_outlined),\n            itemBuilder: (BuildContext context) {\n              return <PopupMenuEntry>[\n                CustomPopupMenuItem(\n                    height: 32,\n                    child: ListTile(\n                        leading: const Icon(Icons.qr_code_scanner_outlined),\n                        dense: true,\n                        title: Text(localizations.scanCode),\n                        onTap: () {\n                          Navigator.maybePop(context);\n                          connectRemote();\n                        })),\n                CustomPopupMenuItem(\n                    height: 32,\n                    child: ListTile(\n                        leading: const Icon(Icons.edit_rounded),\n                        dense: true,\n                        title: Text(localizations.inputAddress),\n                        onTap: () async {\n                          Navigator.maybePop(context);\n                          inputAddress(await localIp());\n                        })),\n                PopupMenuItem(\n                    height: 32,\n                    child: ListTile(\n                      dense: true,\n                      leading: const Icon(Icons.phone_android),\n                      title: Text(localizations.myQRCode),\n                      onTap: () async {\n                        Navigator.maybePop(context);\n                        var ip = await localIp(readCache: false);\n                        if (context.mounted) {\n                          qrCode(context, ip, widget.proxyServer.port);\n                        }\n                      },\n                    )),\n              ];\n            },\n          ),\n          const SizedBox(width: 10),\n        ],\n      ),\n      body: Padding(\n        padding: const EdgeInsets.all(16.0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            remoteDeviceStatus(), //远程设备状态\n            const SizedBox(height: 20),\n            Text(localizations.remoteDeviceList, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n            const SizedBox(height: 10),\n            Expanded(child: futureWidget(SharedPreferences.getInstance(), rows)), //远程设备列表\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget rows(SharedPreferences prefs) {\n    var remoteDeviceList = getRemoteDeviceList(prefs);\n\n    return ListView(\n      children: remoteDeviceList.map((remoteDevice) {\n        return Dismissible(\n            key: Key(remoteDevice.identification),\n            onDismissed: (direction) async {\n              remoteDeviceList.removeWhere((it) => it.equals(remoteDevice));\n              await setRemoteDeviceList(prefs, remoteDeviceList);\n\n              setState(() {});\n              if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);\n            },\n            child: ListTile(\n              contentPadding: const EdgeInsets.symmetric(horizontal: 5),\n              title: Text(remoteDevice.hostname ?? ''),\n              subtitle: Text('${remoteDevice.host}:${remoteDevice.port}'),\n              trailing: getIcon(remoteDevice.os!),\n              onTap: () {\n                doConnect(remoteDevice.host!, remoteDevice.port!, ipProxy: remoteDevice.ipProxy);\n              },\n            ));\n      }).toList(),\n    );\n  }\n\n  Icon getIcon(String os) {\n    if (os.contains(\"windows\")) {\n      return const Icon(Icons.window_sharp, size: 30);\n    } else if (os.contains(\"linux\")) {\n      return const Icon(Icons.desktop_windows, size: 30);\n    } else if (os.contains(\"macos\") || os.contains(\"ios\")) {\n      return const Icon(Icons.apple, size: 30);\n    } else if (os == 'android') {\n      return const Icon(Icons.android, size: 30);\n    } else {\n      return const Icon(Icons.devices, size: 30);\n    }\n  }\n\n  List<RemoteModel> getRemoteDeviceList(SharedPreferences prefs) {\n    var remoteDeviceList = prefs.getStringList('remoteDeviceList') ?? [];\n    return remoteDeviceList.map((it) => RemoteModel.fromJson(jsonDecode(it))).toList();\n  }\n\n  Future<bool> setRemoteDeviceList(SharedPreferences prefs, Iterable<RemoteModel> remoteDeviceList) {\n    var list = remoteDeviceList.map((it) => jsonEncode(it.toJson())).toList();\n    return prefs.setStringList('remoteDeviceList', list);\n  }\n\n  ///远程设备状态\n  Widget remoteDeviceStatus() {\n    if (widget.remoteDevice.value.connect) {\n      return Center(\n          child: Column(\n        children: [\n          const Icon(Icons.check_circle_outline_outlined, size: 55, color: Colors.green),\n          const SizedBox(height: 6),\n          if (Platform.isIOS)\n            Row(\n              children: [\n                Expanded(\n                    child: ListTile(\n                        title: Text(localizations.ipLayerProxy), subtitle: Text(localizations.ipLayerProxyDesc))),\n                SwitchWidget(\n                    value: widget.remoteDevice.value.ipProxy ?? false,\n                    scale: 0.85,\n                    onChanged: (val) async {\n                      widget.remoteDevice.value = widget.remoteDevice.value.copyWith(ipProxy: val);\n                      SharedPreferences.getInstance().then((prefs) {\n                        var remoteDeviceList = getRemoteDeviceList(prefs);\n                        remoteDeviceList.removeWhere((it) => it.equals(widget.remoteDevice.value));\n                        remoteDeviceList.insert(0, widget.remoteDevice.value);\n\n                        setRemoteDeviceList(prefs, remoteDeviceList);\n                      });\n\n                      if ((await Vpn.isRunning())) {\n                        Vpn.stopVpn();\n                        Future.delayed(const Duration(milliseconds: 1500), () {\n                          Vpn.startVpn(widget.remoteDevice.value.host!, widget.remoteDevice.value.port!,\n                              widget.proxyServer.configuration,\n                              ipProxy: val);\n                        });\n                      }\n                    }),\n              ],\n            ),\n          const SizedBox(height: 6),\n          Text('${localizations.connected}：${widget.remoteDevice.value.hostname}',\n              style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n          const SizedBox(height: 6),\n          Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [\n            TextButton.icon(\n              style: ButtonStyle(\n                  shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                      RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)))),\n              onPressed: pullConfig,\n              icon: const Icon(Icons.sync),\n              label: Text(localizations.syncConfig),\n            ),\n            TextButton.icon(\n              label: Text(localizations.disconnect),\n              style: ButtonStyle(\n                shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                    RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0))),\n              ),\n              icon: const Icon(Icons.cancel_outlined),\n              onPressed: () {\n                widget.remoteDevice.value = RemoteModel(connect: false);\n                setState(() {});\n              },\n            ),\n          ])\n        ],\n      ));\n    }\n\n    return Center(\n        child: Column(children: [\n      const Icon(Icons.cancel_outlined, size: 55, color: Colors.red),\n      const SizedBox(height: 6),\n      Text(localizations.notConnected, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n    ]));\n  }\n\n  ///输入地址链接\n  inputAddress(var host) {\n    //输入账号密码连接\n    host = host.substring(0, host.contains('.') ? host.lastIndexOf('.') + 1 : host.length);\n    int? port = 9099;\n    if (!context.mounted) return;\n\n    showDialog(\n        context: context,\n        builder: (context) {\n          return AlertDialog(\n            title: Text(localizations.inputAddress),\n            content: Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                TextFormField(\n                  initialValue: host,\n                  decoration: const InputDecoration(hintText: 'Host'),\n                  keyboardType: TextInputType.url,\n                  onChanged: (value) => host = value,\n                ),\n                TextFormField(\n                    initialValue: port.toString(),\n                    decoration: const InputDecoration(hintText: 'Port'),\n                    keyboardType: TextInputType.number,\n                    onChanged: (value) {\n                      port = value.isEmpty ? null : int.tryParse(value);\n                    }),\n              ],\n            ),\n            actions: [\n              TextButton(\n                  onPressed: () {\n                    Navigator.pop(context);\n                  },\n                  child: Text(localizations.cancel)),\n              TextButton(\n                  onPressed: () async {\n                    if (host.isEmpty || port == null) {\n                      FlutterToastr.show(localizations.cannotBeEmpty, context);\n                      return;\n                    }\n\n                    if ((await doConnect(host, port!)) && context.mounted) {\n                      Navigator.pop(context);\n                    }\n                  },\n                  child: Text(localizations.connectRemote)),\n            ],\n          );\n        });\n  }\n\n  ///扫码连接\n  connectRemote() async {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n    String? scanRes = await QrCodeScanner.scan(context);\n    if (scanRes == null) return;\n\n    if (scanRes == \"-1\") {\n      if (context.mounted) FlutterToastr.show(localizations.invalidQRCode, context);\n      return;\n    }\n\n    if (scanRes.startsWith(\"http\")) {\n      launchUrl(Uri.parse(scanRes), mode: LaunchMode.externalApplication);\n      return;\n    }\n\n    if (scanRes.startsWith(\"proxypin://connect\")) {\n      Uri uri = Uri.parse(scanRes);\n      var host = uri.queryParameters['host'];\n      var port = uri.queryParameters['port'];\n\n      doConnect(host!, int.parse(port!));\n      return;\n    }\n\n    if (mounted) {\n      FlutterToastr.show(localizations.invalidQRCode, context);\n    }\n  }\n\n  ///\n  bool doConnecting = false;\n\n  ///连接远程设备\n  Future<bool> doConnect(String host, int port, {bool? ipProxy}) async {\n    if (doConnecting) return false;\n    doConnecting = true;\n\n    try {\n      var response = await HttpClients.get(\"http://$host:$port/ping\", timeout: const Duration(milliseconds: 3000));\n      if (response.bodyAsString == \"pong\") {\n        widget.remoteDevice.value = RemoteModel(\n          connect: true,\n          host: host,\n          port: port,\n          os: response.headers.get(\"os\"),\n          hostname: response.headers.get(\"hostname\"),\n          ipProxy: ipProxy,\n        );\n\n        //去重记录5条连接记录\n        SharedPreferences prefs = await SharedPreferences.getInstance();\n        var remoteDeviceList = getRemoteDeviceList(prefs);\n        remoteDeviceList.removeWhere((it) => it.equals(widget.remoteDevice.value));\n        remoteDeviceList.insert(0, widget.remoteDevice.value);\n\n        var list = remoteDeviceList.take(5);\n        setRemoteDeviceList(prefs, list).whenComplete(() {\n          setState(() {});\n        });\n\n        if (mounted) {\n          CustomToast.success(\n                  \"${localizations.connectSuccess}${Vpn.isVpnStarted ? '' : ', ${localizations.remoteConnectSuccessTips}'}\")\n              .show(context);\n        }\n      }\n      return true;\n    } catch (e) {\n      logger.e(e);\n      if (mounted) {\n        CustomToast.error(localizations.remoteConnectFail).show(context, alignment: Alignment.topCenter);\n      }\n      return false;\n    } finally {\n      doConnecting = false;\n    }\n  }\n\n  ///连接二维码\n  qrCode(BuildContext context, String host, int port) {\n    AppLocalizations localizations = AppLocalizations.of(context)!;\n\n    showDialog(\n        context: context,\n        builder: (context) {\n          return AlertDialog(\n            contentPadding: const EdgeInsets.all(15),\n            actionsPadding: const EdgeInsets.only(bottom: 10, right: 10),\n            title: Text(localizations.remoteConnectForward, style: const TextStyle(fontSize: 16)),\n            content: SizedBox(\n                height: 280,\n                width: 300,\n                child: Column(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    QrImageView(\n                        backgroundColor: Colors.white,\n                        data: \"proxypin://connect?host=$host&port=${widget.proxyServer.port}\",\n                        version: QrVersions.auto,\n                        size: 200.0),\n                    const SizedBox(height: 10),\n                    Row(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        Text('${localizations.localIP}:'),\n                        const SizedBox(width: 5),\n                        SelectableText('$host:$port'),\n                      ],\n                    ),\n                    const SizedBox(height: 10),\n                    Text(localizations.mobileScan),\n                  ],\n                )),\n            actions: [\n              TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(localizations.cancel)),\n            ],\n          );\n        });\n  }\n\n  //拉取桌面配置\n  pullConfig() {\n    var desktopModel = widget.remoteDevice.value;\n    HttpClients.get('http://${desktopModel.host}:${desktopModel.port}/config').then((response) {\n      if (response.status.isSuccessful() && mounted) {\n        var config = jsonDecode(response.bodyAsString);\n        syncConfig = true;\n        showDialog(\n            context: context,\n            builder: (context) {\n              return ConfigSyncWidget(configuration: widget.proxyServer.configuration, config: config);\n            });\n      }\n    }).onError((error, stackTrace) {\n      logger.e('拉取配置失败', error: error, stackTrace: stackTrace);\n      if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(localizations.pullConfigFail)));\n    });\n  }\n}\n\nclass ConfigSyncWidget extends StatefulWidget {\n  final Configuration configuration;\n  final Map<String, dynamic> config;\n\n  const ConfigSyncWidget({super.key, required this.configuration, required this.config});\n\n  @override\n  State<StatefulWidget> createState() {\n    return ConfigSyncState();\n  }\n}\n\nclass ConfigSyncState extends State<ConfigSyncWidget> {\n  bool syncWhiteList = true;\n  bool syncBlackList = true;\n  bool syncRewrite = true;\n  bool syncScript = true;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n      title: Text(localizations.syncConfig, style: const TextStyle(fontSize: 16)),\n      content: Wrap(children: [\n        SwitchWidget(\n            title: \"${localizations.sync} ${localizations.domainWhitelist}\",\n            value: syncWhiteList,\n            onChanged: (val) {\n              setState(() {\n                syncWhiteList = val;\n              });\n            }),\n        const SizedBox(height: 5),\n        SwitchWidget(\n            title: \"${localizations.sync} ${localizations.domainBlacklist}\",\n            value: syncBlackList,\n            onChanged: (val) {\n              setState(() {\n                syncBlackList = val;\n              });\n            }),\n        const SizedBox(height: 5),\n        SwitchWidget(\n            title: \"${localizations.sync} ${localizations.requestRewrite}\",\n            value: syncRewrite,\n            onChanged: (val) {\n              setState(() {\n                syncRewrite = val;\n              });\n            }),\n        const SizedBox(height: 5),\n        SwitchWidget(\n            title: \"${localizations.sync} ${localizations.script}\",\n            value: syncScript,\n            onChanged: (val) {\n              setState(() {\n                syncScript = val;\n              });\n            }),\n      ]),\n      actions: [\n        TextButton(\n            child: Text(localizations.cancel),\n            onPressed: () {\n              Navigator.pop(context);\n            }),\n        TextButton(\n            child: Text('${localizations.start} ${localizations.sync}'),\n            onPressed: () async {\n              if (syncWhiteList) {\n                HostFilter.whitelist.load(widget.config['whitelist']);\n              }\n              if (syncBlackList) {\n                HostFilter.blacklist.load(widget.config['blacklist']);\n              }\n              widget.configuration.flushConfig();\n\n              if (syncRewrite) {\n                var requestRewrites = await RequestRewriteManager.instance;\n                await requestRewrites.syncConfig(widget.config['requestRewrites']);\n              }\n\n              if (syncScript) {\n                var scriptManager = await ScriptManager.instance;\n                await scriptManager.clean();\n                scriptManager.list.clear();\n                for (var item in widget.config['scripts']) {\n                  await scriptManager.addScript(ScriptItem.fromJson(item), item['script']);\n                }\n                await scriptManager.flushConfig();\n              }\n\n              if (mounted) {\n                Navigator.pop(this.context);\n                ScaffoldMessenger.of(this.context)\n                    .showSnackBar(SnackBar(content: Text('${localizations.sync}${localizations.success}')));\n              }\n            }),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/toolbox/aes_page.dart",
    "content": "import 'dart:convert';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\nimport '../component/buttons.dart';\nimport '../component/text_field.dart';\nimport '../../utils/aes.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\n\nclass AesPage extends StatefulWidget {\n  const AesPage({super.key});\n\n  @override\n  State<AesPage> createState() => _AesWidgetState();\n}\n\nclass _AesWidgetState extends State<AesPage> {\n  final TextEditingController inputController = TextEditingController();\n  final TextEditingController outputController = TextEditingController();\n  final TextEditingController keyController = TextEditingController();\n  final TextEditingController ivController = TextEditingController();\n\n  String selectedMode = 'ECB';\n  String selectedPadding = 'PKCS7';\n  int selectedKeyLength = 128;\n  final List<String> modes = ['ECB', 'CBC'];\n  final List<String> paddingModes = ['PKCS7', 'ZeroPadding'];\n  final List<int> keyLengths = [128, 192, 256];\n\n  void encryptText() {\n    try {\n      final input = Uint8List.fromList(utf8.encode(inputController.text));\n      final encrypted = AesUtils.encrypt(input,\n          key: keyController.text,\n          mode: selectedMode,\n          iv: ivController.text,\n          keyLength: selectedKeyLength,\n          padding: selectedPadding);\n      outputController.text = base64.encode(encrypted);\n    } catch (e) {\n      logger.e(\"Encryption error: $e\");\n      FlutterToastr.show(\"Encryption failed\", context, duration: 3, backgroundColor: Colors.red);\n    }\n  }\n\n  void decryptText() {\n    try {\n      final input = base64.decode(inputController.text);\n      final decrypted = AesUtils.decrypt(input,\n          key: keyController.text,\n          mode: selectedMode,\n          iv: ivController.text,\n          keyLength: selectedKeyLength,\n          padding: selectedPadding);\n      outputController.text = utf8.decode(decrypted);\n    } catch (e) {\n      outputController.text = \"\";\n      logger.e(\"Decryption error: $e\");\n      FlutterToastr.show(\"Decryption failed\", context, duration: 3, backgroundColor: Colors.red);\n    }\n  }\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text(\"AES\", style: TextStyle(fontSize: 16)), centerTitle: true),\n      body: Padding(\n        padding: const EdgeInsets.all(15),\n        child: ListView(children: [\n          const SizedBox(height: 5),\n          SizedBox(\n              height: 150,\n              child: TextField(\n                  controller: inputController,\n                  maxLines: 8,\n                  onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n                  decoration: decoration(context, label: localizations.inputContent))),\n          const SizedBox(height: 15),\n          Wrap(spacing: 18, runSpacing: 5, crossAxisAlignment: WrapCrossAlignment.center, children: [\n            SizedBox(\n                width: 120,\n                child: Row(children: [\n                  Text(\"Mode\"),\n                  const SizedBox(width: 15),\n                  DropdownButton<String>(\n                    value: selectedMode,\n                    items: modes.map((mode) {\n                      return DropdownMenuItem(value: mode, child: Text(mode));\n                    }).toList(),\n                    onChanged: (value) {\n                      setState(() {\n                        selectedMode = value!;\n                      });\n                    },\n                  ),\n                ])),\n            SizedBox(\n                width: 195,\n                child: Row(children: [\n                  Text(\"Padding\"),\n                  const SizedBox(width: 15),\n                  DropdownButton<String>(\n                    value: selectedPadding,\n                    items: paddingModes.map((mode) {\n                      return DropdownMenuItem(value: mode, child: Text(mode));\n                    }).toList(),\n                    onChanged: (value) {\n                      setState(() {\n                        selectedPadding = value!;\n                      });\n                    },\n                  ),\n                ])),\n            SizedBox(\n                width: 190,\n                child: Row(children: [\n                  Text(\"Key Length\"),\n                  const SizedBox(width: 15),\n                  DropdownButton<int>(\n                    value: selectedKeyLength,\n                    items: keyLengths.map((length) {\n                      return DropdownMenuItem(value: length, child: Text(\"$length bits\"));\n                    }).toList(),\n                    onChanged: (value) {\n                      setState(() {\n                        selectedKeyLength = value!;\n                      });\n                    },\n                  ),\n                ]))\n          ]),\n          const SizedBox(height: 15),\n          Wrap(\n              spacing: 18.0, // 主轴方向子组件的间距\n              runSpacing: 10.0, // 交叉轴方向子组件的间距\n              crossAxisAlignment: WrapCrossAlignment.center,\n              children: [\n                SizedBox(\n                    width: 230,\n                    child: Row(children: [\n                      const SizedBox(width: 25, child: Text(\"Key\")),\n                      const SizedBox(width: 15),\n                      SizedBox(\n                          width: 180,\n                          height: 45,\n                          child: TextField(\n                              controller: keyController,\n                              maxLength: 64,\n                              onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n                              style: TextStyle(fontSize: 14),\n                              decoration: InputDecoration(\n                                  border: const OutlineInputBorder(),\n                                  counterText: \"\",\n                                  contentPadding: const EdgeInsets.symmetric(horizontal: 6)))),\n                    ])),\n                SizedBox(\n                    width: 260,\n                    child: Row(children: [\n                      const SizedBox(width: 25, child: Text(\"IV\")),\n                      const SizedBox(width: 15),\n                      SizedBox(\n                          width: 180,\n                          height: 45,\n                          child: TextField(\n                              controller: ivController,\n                              maxLength: 32,\n                              onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n                              style: TextStyle(fontSize: 14),\n                              decoration: InputDecoration(\n                                  border: const OutlineInputBorder(),\n                                  counterText: \"\",\n                                  contentPadding: const EdgeInsets.symmetric(horizontal: 6)))),\n                    ])),\n              ]),\n          const SizedBox(height: 20),\n          Row(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              FilledButton(\n                  style: ButtonStyle(\n                      shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                          RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),\n                  onPressed: encryptText,\n                  child: Text(localizations.encrypt)),\n              const SizedBox(width: 60),\n              FilledButton(\n                  style: ButtonStyle(\n                      shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                          RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),\n                  onPressed: decryptText,\n                  child: Text(localizations.decrypt)),\n            ],\n          ),\n          const SizedBox(height: 5),\n          Text(localizations.output),\n          const SizedBox(height: 5),\n          TextFormField(\n            controller: outputController,\n            readOnly: true,\n            minLines: 5,\n            maxLines: 10,\n            decoration: const InputDecoration(border: OutlineInputBorder()),\n          ),\n          const SizedBox(height: 10),\n          FilledButton.icon(\n            style: Buttons.buttonStyle,\n            onPressed: () {\n              Clipboard.setData(ClipboardData(text: outputController.text));\n              FlutterToastr.show(localizations.copied, context);\n            },\n            icon: const Icon(Icons.copy),\n            label: Text(localizations.copy),\n          ),\n        ]),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/toolbox/cert_hash.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/util/cert/x509.dart';\nimport 'package:proxypin/ui/component/buttons.dart';\nimport 'package:proxypin/ui/component/text_field.dart';\nimport 'package:proxypin/utils/platform.dart';\n\n///证书哈希名称查看\n///@author Hongen Wang\nclass CertHashPage extends StatefulWidget {\n  final int? windowId;\n\n  const CertHashPage({super.key, this.windowId});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _CertHashPageState();\n  }\n}\n\nclass _CertHashPageState extends State<CertHashPage> {\n  var input = TextEditingController();\n  TextEditingController decodeData = TextEditingController();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (Platforms.isDesktop() && widget.windowId != null) {\n      HardwareKeyboard.instance.addHandler(onKeyEvent);\n    }\n  }\n\n  @override\n  void dispose() {\n    input.dispose();\n    decodeData.dispose();\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if (widget.windowId == null) return false;\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      WindowController.fromWindowId(widget.windowId!).close();\n      return true;\n    }\n\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(title: Text(localizations.systemCertName, style: TextStyle(fontSize: 16)), centerTitle: true),\n        resizeToAvoidBottomInset: false,\n        body: ListView(children: [\n          Wrap(alignment: WrapAlignment.end, children: [\n            ElevatedButton.icon(\n                onPressed: () async {\n                  FilePickerResult? result = await FilePicker.platform\n                      .pickFiles(type: FileType.custom, allowedExtensions: ['crt', 'pem', 'cer', 'der']);\n                  if (result == null) return;\n\n                  File file = File(result.files.single.path!);\n                  var bytes = await file.readAsBytes();\n                  input.text = tryDerFormat(bytes) ?? String.fromCharCodes(bytes);\n                  getSubjectName();\n                },\n                style: Buttons.buttonStyle,\n                icon: const Icon(Icons.folder_open),\n                label: Text(\"File\")),\n            const SizedBox(width: 15),\n            ElevatedButton.icon(\n                onPressed: () => input.clear(),\n                style: Buttons.buttonStyle,\n                icon: const Icon(Icons.clear),\n                label: const Text(\"Clear\")),\n            const SizedBox(width: 15),\n            FilledButton.icon(\n                onPressed: () {\n                  getSubjectName();\n                  FocusScope.of(context).unfocus();\n                },\n                style: Buttons.buttonStyle,\n                icon: const Icon(Icons.play_arrow_rounded),\n                label: const Text(\"Run\")),\n            const SizedBox(width: 15),\n          ]),\n          const SizedBox(width: 10),\n          Container(\n              padding: const EdgeInsets.all(10),\n              height: 350,\n              child: TextFormField(\n                  maxLines: 50,\n                  controller: input,\n                  onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n                  keyboardType: TextInputType.text,\n                  decoration: decoration(context, label: localizations.inputContent))),\n          Align(\n              alignment: Alignment.bottomLeft,\n              child: TextButton(\n                  onPressed: () {}, child: Text(\"${localizations.output}:\", style: TextStyle(fontSize: 16)))),\n          Container(\n              width: double.infinity,\n              padding: const EdgeInsets.all(10),\n              height: 150,\n              child: TextFormField(\n                  maxLines: 30,\n                  readOnly: true,\n                  controller: decodeData,\n                  decoration: decoration(context, label: 'Android ${localizations.systemCertName}'))),\n        ]));\n  }\n\n  getSubjectName() {\n    var content = input.text;\n    if (content.isEmpty) return;\n    try {\n      var caCert = X509Utils.x509CertificateFromPem(content);\n      var subject = caCert.subject;\n      var subjectHashName = X509Utils.getSubjectHashName(subject);\n      decodeData.text = '$subjectHashName.0';\n    } catch (e) {\n      FlutterToastr.show(localizations.decodeFail, context, duration: 3, backgroundColor: Colors.red);\n    }\n  }\n\n  String? tryDerFormat(Uint8List data) {\n    try {\n      final bytes = data.sublist(0, 4);\n\n      // Check if the bytes match the DER format (ASN.1 encoding)\n      // DER encoded certificates typically start with 0x30 (SEQUENCE) or 0xA0 (APPLICATION)\n      if (bytes[0] == 0x30 || bytes[0] == 0xA0) {\n        return X509Utils.crlDerToPem(data);\n      }\n      return null;\n    } catch (e) {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/toolbox/encoder.dart",
    "content": "import 'dart:convert';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/network/util/logger.dart';\n\n///编码类型\nenum EncoderType {\n  url,\n  base64,\n  unicode,\n  md5;\n\n  static EncoderType nameOf(String name) {\n    for (var value in values) {\n      if (value.name == name) {\n        return value;\n      }\n    }\n    return url;\n  }\n}\n\nclass EncoderWidget extends StatefulWidget {\n  final EncoderType type;\n  final WindowController? windowController;\n  final String? text;\n\n  const EncoderWidget({super.key, required this.type, this.windowController, this.text});\n\n  @override\n  State<EncoderWidget> createState() => _EncoderState();\n}\n\nclass _EncoderState extends State<EncoderWidget> with SingleTickerProviderStateMixin {\n  var tabs = const [\n    Tab(text: 'URL'),\n    Tab(text: 'Base64'),\n    Tab(text: 'Unicode'),\n    Tab(text: 'MD5'),\n  ];\n\n  late EncoderType type;\n  late TabController tabController;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  String inputText = '';\n  TextEditingController outputTextController = TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n    type = widget.type;\n    inputText = widget.text ?? '';\n\n    tabController = TabController(initialIndex: type.index, length: tabs.length, vsync: this);\n    HardwareKeyboard.instance.addHandler(onKeyEvent);\n  }\n\n  @override\n  void dispose() {\n    tabController.dispose();\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      tabController.dispose();\n      widget.windowController?.close();\n      return true;\n    }\n\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      resizeToAvoidBottomInset: true,\n      appBar: AppBar(\n          title: Text('${type.name.toUpperCase()}${localizations.encode}', style: const TextStyle(fontSize: 16)),\n          centerTitle: true,\n          bottom: TabBar(\n            controller: tabController,\n            tabs: tabs,\n            onTap: (index) {\n              setState(() {\n                type = EncoderType.values[index];\n                outputTextController.clear();\n              });\n            },\n          )),\n      body: Container(\n        padding: const EdgeInsets.all(10),\n        child: ListView(\n          children: <Widget>[\n            Text(localizations.encodeInput),\n            const SizedBox(height: 5),\n            TextFormField(\n                initialValue: inputText,\n                minLines: 5,\n                maxLines: 10,\n                onChanged: (text) => inputText = text,\n                decoration: const InputDecoration(border: OutlineInputBorder())),\n            const SizedBox(height: 10),\n            Wrap(\n              alignment: WrapAlignment.center,\n              children: [\n                FilledButton(onPressed: encode, child: Text('${type.name.toUpperCase()}${localizations.encode}')),\n                const SizedBox(width: 20),\n                type == EncoderType.md5\n                    ? const SizedBox()\n                    : OutlinedButton(\n                        onPressed: decode, child: Text('${type.name.toUpperCase()}${localizations.decode}')),\n              ],\n            ),\n            Text(localizations.encodeResult),\n            const SizedBox(height: 5),\n            TextFormField(\n              controller: outputTextController,\n              readOnly: true,\n              minLines: 5,\n              maxLines: 10,\n              decoration: const InputDecoration(border: OutlineInputBorder()),\n            ),\n            const SizedBox(height: 10),\n            ElevatedButton(\n              onPressed: () {\n                Clipboard.setData(ClipboardData(text: outputTextController.text));\n                FlutterToastr.show(localizations.copied, context);\n              },\n              child: Text(localizations.copy),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  ///编码\n  void encode() {\n    var result = '';\n    try {\n      switch (type) {\n        case EncoderType.url:\n          result = Uri.encodeFull(inputText);\n        case EncoderType.base64:\n          result = base64.encode(utf8.encode(inputText));\n        case EncoderType.md5:\n          result = md5.convert(utf8.encode(inputText)).toString();\n        case EncoderType.unicode:\n          result = encodeToUnicode(inputText);\n      }\n    } catch (e) {\n      FlutterToastr.show(localizations.encodeFail, context);\n    }\n    outputTextController.text = result;\n  }\n\n  ///解码\n  void decode() {\n    var result = '';\n    try {\n      switch (type) {\n        case EncoderType.url:\n          result = Uri.decodeFull(inputText);\n        case EncoderType.base64:\n          // base64.\n          var text = inputText.replaceAll('.', '');\n          if (text.length % 4 != 0) {\n            text = text.padRight(text.length + (4 - text.length % 4), '=');\n          }\n          Uint8List compressed = base64.decode(text);\n          try {\n            result = utf8.decode(compressed);\n          } catch (e) {\n            result = String.fromCharCodes(compressed);\n          }\n        case EncoderType.md5:\n        case EncoderType.unicode:\n          result = decodeFromUnicode(inputText);\n      }\n    } catch (e, t) {\n      logger.e(\"$e\", error: e, stackTrace: t);\n      FlutterToastr.show(localizations.decodeFail, context);\n    }\n    outputTextController.text = result;\n  }\n\n  String encodeToUnicode(String input) {\n    return input.runes.map((rune) => '\\\\u${rune.toRadixString(16).padLeft(4, '0')}').join();\n  }\n\n  String decodeFromUnicode(String input) {\n    return input.replaceAllMapped(RegExp(r'\\\\u([0-9a-fA-F]{4})'), (match) {\n      return String.fromCharCode(int.parse(match.group(1)!, radix: 16));\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/toolbox/js_run.dart",
    "content": "import 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_code_editor/flutter_code_editor.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_highlight/themes/monokai-sublime.dart';\nimport 'package:flutter_js/flutter_js.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:highlight/languages/javascript.dart';\nimport 'package:proxypin/network/components/js/file.dart';\nimport 'package:proxypin/network/components/js/md5.dart';\nimport 'package:proxypin/network/components/js/xhr.dart';\n\nclass JavaScript extends StatefulWidget {\n  final int? windowId;\n\n  const JavaScript({super.key, this.windowId});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _JavaScriptState();\n  }\n}\n\nclass _JavaScriptState extends State<JavaScript> {\n  //重置环境\n  static bool resetEnvironment = true;\n\n  static JavascriptRuntime? flutterJs;\n\n  late CodeController code;\n\n  List<Text> outLines = [];\n\n  ScrollController inputScrollController = ScrollController();\n  ScrollController outputScrollController = ScrollController();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (resetEnvironment || flutterJs == null) {\n      flutterJs = getJavascriptRuntime(xhr: false);\n    }\n    // register channel callback\n    final channelCallbacks = JavascriptRuntime.channelFunctionsRegistered[flutterJs!.getEngineInstanceId()];\n    channelCallbacks![\"ConsoleLog\"] = consoleLog;\n    Md5Bridge.registerMd5(flutterJs!);\n    FileBridge.registerFile(flutterJs!);\n    flutterJs?.enableFetch2(enabledProxy: true);\n\n    code = CodeController(language: javascript, text: 'console.log(\"Hello, World!\")');\n  }\n\n  @override\n  void dispose() {\n    code.dispose();\n    inputScrollController.dispose();\n    outputScrollController.dispose();\n    if (resetEnvironment) {\n      flutterJs?.dispose();\n      flutterJs = null;\n    }\n    super.dispose();\n  }\n\n  dynamic consoleLog(dynamic args) async {\n    var level = args.removeAt(0);\n    String output = args.join(' ');\n    if (level == 'info') level = 'warn';\n    setState(() {\n      outLines.add(Text(output, style: TextStyle(color: level == 'error' ? Colors.red : Colors.white, fontSize: 13)));\n      print(outLines);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Color primaryColor = Theme.of(context).colorScheme.primary;\n    return Scaffold(\n        resizeToAvoidBottomInset: false,\n        appBar: AppBar(title: const Text(\"JavaScript\", style: TextStyle(fontSize: 16)), centerTitle: true),\n        body: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n          Row(\n            mainAxisAlignment: MainAxisAlignment.end,\n            children: [\n              //选择文件\n              ElevatedButton.icon(\n                  onPressed: () async {\n                    String? path;\n                    if (Platform.isMacOS) {\n                      path = await DesktopMultiWindow.invokeMethod(0, \"pickFiles\", {\n                        \"allowedExtensions\": ['js']\n                      });\n                      WindowController.fromWindowId(widget.windowId!).show();\n                    } else {\n                      FilePickerResult? result =\n                          await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['js']);\n                      path = result?.files.single.path;\n                    }\n\n                    if (path != null) {\n                      File file = File(path);\n                      String content = await file.readAsString();\n                      code.text = content;\n                      setState(() {});\n                    }\n                  },\n                  icon: const Icon(Icons.folder_open),\n                  label: const Text(\"File\")),\n              const SizedBox(width: 15),\n              FilledButton.icon(\n                  onPressed: () async {\n                    outLines.clear();\n                    //失去焦点\n                    FocusScope.of(context).unfocus();\n                    var jsResult = await flutterJs!.evaluateAsync(code.text);\n                    if (jsResult.isPromise || jsResult.rawResult is Future) {\n                      jsResult = await flutterJs!.handlePromise(jsResult);\n                    }\n                    if (jsResult.isError) {\n                      setState(() {\n                        outLines\n                            .add(Text(jsResult.toString(), style: const TextStyle(color: Colors.red, fontSize: 13)));\n                      });\n                    }\n                  },\n                  icon: const Icon(Icons.play_arrow_rounded),\n                  label: const Text(\"Run\")),\n              const SizedBox(width: 10),\n            ],\n          ),\n          const SizedBox(height: 10),\n          SizedBox(\n              height: 320,\n              child: CodeTheme(\n                  data: CodeThemeData(styles: monokaiSublimeTheme),\n                  child: Scrollbar(\n                      controller: inputScrollController,\n                      thumbVisibility: true,\n                      interactive: true,\n                      trackVisibility: true,\n                      thickness: 8,\n                      child: SingleChildScrollView(\n                          controller: inputScrollController,\n                          scrollDirection: Axis.vertical,\n                          child: CodeField(\n                            minLines: 16,\n                            background: Colors.grey.shade800,\n                            padding: const EdgeInsets.only(right: 10),\n                            textStyle: const TextStyle(fontSize: 13),\n                            controller: code,\n                            enableSuggestions: true,\n                            onTapOutside: (event) => FocusScope.of(context).unfocus(),\n                            gutterStyle: const GutterStyle(width: 50, margin: 0),\n                          ))))),\n          Row(children: [\n            const SizedBox(width: 10),\n            Text(\"${localizations.output}:\",\n                style: TextStyle(fontSize: 16, color: primaryColor, fontWeight: FontWeight.w500)),\n            const SizedBox(width: 15),\n            //copy\n            IconButton(\n                icon: Icon(Icons.copy, color: primaryColor, size: 18),\n                onPressed: () {\n                  Clipboard.setData(ClipboardData(text: outLines.join(\"\\n\")));\n                  FlutterToastr.show(localizations.copied, context, duration: 3);\n                }),\n          ]),\n          Expanded(\n              child: Container(\n                  width: double.infinity,\n                  padding: const EdgeInsets.all(10),\n                  color: Colors.grey.shade800,\n                  child: Scrollbar(\n                      controller: outputScrollController,\n                      thumbVisibility: true,\n                      trackVisibility: true,\n                      child: SingleChildScrollView(\n                          controller: outputScrollController,\n                          child: SelectionArea(\n                              child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: outLines)))))),\n        ]));\n  }\n}\n\n// Create a new widget for the fullscreen CodeField\nclass FullScreenCodeField extends StatelessWidget {\n  final CodeController code;\n\n  FullScreenCodeField({required this.code});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n          title: Text(\"FullScreen Code Editor\"),\n          actions: [\n            IconButton(\n              icon: Icon(Icons.close),\n              onPressed: () {\n                Navigator.of(context).pop();\n              },\n            ),\n          ],\n        ),\n        body: Expanded(\n          child: CodeTheme(\n            data: CodeThemeData(styles: monokaiSublimeTheme),\n            child: CodeField(\n              background: Colors.grey.shade800,\n              minLines: 50,\n              textStyle: const TextStyle(fontSize: 12),\n              controller: code,\n              gutterStyle: const GutterStyle(width: 50, margin: 0),\n            ),\n          ),\n        ));\n  }\n}\n"
  },
  {
    "path": "lib/ui/toolbox/qr_code_page.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'dart:async';\nimport 'dart:io';\nimport 'dart:ui' as ui;\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_qr_reader_plus/flutter_qr_reader.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:image_pickers/image_pickers.dart';\nimport 'package:proxypin/ui/component/app_dialog.dart';\nimport 'package:proxypin/ui/component/qrcode/qr_scan_view.dart';\nimport 'package:proxypin/ui/component/text_field.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:qr_flutter/qr_flutter.dart';\n\n///二维码\n///@author Hongen Wang\nclass QrCodePage extends StatefulWidget {\n  final int? windowId;\n\n  const QrCodePage({super.key, this.windowId});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _QrCodePageState();\n  }\n}\n\nclass _QrCodePageState extends State<QrCodePage> with SingleTickerProviderStateMixin {\n  TabController? tabController;\n\n  late List<Tab> tabs = [\n    Tab(text: 'Encode'),\n    Tab(text: 'Decode'),\n  ];\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    if (Platforms.isMobile()) {\n      tabController = TabController(initialIndex: 0, length: tabs.length, vsync: this);\n    }\n\n    if (Platforms.isDesktop() && widget.windowId != null) {\n      HardwareKeyboard.instance.addHandler(onKeyEvent);\n    }\n  }\n\n  @override\n  void dispose() {\n    tabController?.dispose();\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if (widget.windowId == null) return false;\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      WindowController.fromWindowId(widget.windowId!).close();\n      return true;\n    }\n\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (Platforms.isDesktop()) {\n      return Scaffold(\n          appBar: AppBar(title: Text(localizations.qrCode, style: TextStyle(fontSize: 16)), centerTitle: true),\n          body: _QrEncode(windowId: widget.windowId));\n    }\n\n    tabs = [\n      Tab(text: localizations.encode),\n      Tab(text: localizations.decode),\n    ];\n\n    return Scaffold(\n        appBar: AppBar(\n            title: Text(localizations.qrCode, style: TextStyle(fontSize: 16)),\n            centerTitle: true,\n            bottom: TabBar(tabs: tabs, controller: tabController)),\n        resizeToAvoidBottomInset: false,\n        body: TabBarView(\n          controller: tabController,\n          children: [_QrEncode(windowId: widget.windowId), _QrDecode(windowId: widget.windowId)],\n        ));\n  }\n}\n\nclass _QrDecode extends StatefulWidget {\n  final int? windowId;\n\n  const _QrDecode({this.windowId});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _QrDecodeState();\n  }\n}\n\nclass _QrDecodeState extends State<_QrDecode> with AutomaticKeepAliveClientMixin {\n  TextEditingController decodeData = TextEditingController();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void dispose() {\n    decodeData.dispose();\n    super.dispose();\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    return ListView(children: [\n      SizedBox(height: 15),\n      Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          const SizedBox(width: 10),\n          FilledButton.icon(\n              onPressed: () async {\n                String? path = await selectImage();\n                if (path == null) return;\n                var result = await FlutterQrReader.imgScan(path);\n                if (result == null) {\n                  if (context.mounted) FlutterToastr.show(localizations.decodeFail, context, duration: 2);\n                  return;\n                }\n                decodeData.text = result;\n              },\n              icon: const Icon(Icons.photo, size: 18),\n              style: ButtonStyle(\n                  padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),\n                  shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                      RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),\n              label: Text(localizations.selectImage)),\n          const SizedBox(width: 10),\n          if (Platforms.isMobile())\n            FilledButton.icon(\n                onPressed: () async {\n                  var scanRes = await QrCodeScanner.scan(context);\n                  if (scanRes == null) return;\n\n                  if (scanRes == \"-1\") {\n                    if (context.mounted) FlutterToastr.show(localizations.invalidQRCode, context, duration: 2);\n                    return;\n                  }\n                  decodeData.text = scanRes;\n                },\n                style: ButtonStyle(\n                    padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),\n                    shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                        RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),\n                icon: const Icon(Icons.qr_code_scanner_outlined, size: 18),\n                label: Text(localizations.scanQrCode, style: TextStyle(fontSize: 14))),\n          const SizedBox(width: 10),\n        ],\n      ),\n      const SizedBox(height: 20),\n      Container(\n          padding: const EdgeInsets.all(10),\n          height: 300,\n          child: Column(children: [\n            TextField(\n              controller: decodeData,\n              maxLines: 7,\n              minLines: 7,\n              readOnly: true,\n              decoration: decoration(context, label: localizations.encodeResult),\n            ),\n            SizedBox(height: 8),\n            TextButton.icon(\n              icon: const Icon(Icons.copy_rounded),\n              onPressed: () {\n                if (decodeData.text.isEmpty) return;\n                Clipboard.setData(ClipboardData(text: decodeData.text));\n                FlutterToastr.show(localizations.copied, context);\n              },\n              label: Text(localizations.copy),\n            ),\n          ])),\n      SizedBox(height: 10),\n    ]);\n  }\n\n  //选择照片\n  Future<String?> selectImage() async {\n    if (Platforms.isMobile()) {\n      final result = await FilePicker.platform.pickFiles(\n        type: FileType.image,\n        allowMultiple: false,\n      );\n      if (result == null || result.files.isEmpty) return null;\n      return result.files.single.path;\n    }\n\n    if (Platforms.isDesktop()) {\n      //<String>['jpg', 'png', 'jpeg']\n      FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.image);\n      if (result == null || result.files.isEmpty) return null;\n      return result.files.single.path;\n    }\n\n    return null;\n  }\n}\n\nclass _QrEncode extends StatefulWidget {\n  final int? windowId;\n\n  const _QrEncode({this.windowId});\n\n  @override\n  State<StatefulWidget> createState() => _QrEncodeState();\n}\n\n//生成二维码\nclass _QrEncodeState extends State<_QrEncode> with AutomaticKeepAliveClientMixin {\n  var errorCorrectLevel = QrErrorCorrectLevel.M;\n  String? data;\n  TextEditingController inputData = TextEditingController();\n  final GlobalKey imageKey = GlobalKey();\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void dispose() {\n    inputData.dispose();\n    super.dispose();\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    return ListView(children: [\n      Container(\n          padding: const EdgeInsets.all(10),\n          height: 180,\n          child: TextField(\n              controller: inputData,\n              maxLines: 8,\n              onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n              decoration: decoration(context, label: localizations.inputContent))),\n      Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          const SizedBox(width: 10),\n          Row(children: [\n            Text(\"${localizations.errorCorrectLevel}: \"),\n            DropdownButton<int>(\n                value: errorCorrectLevel,\n                items: QrErrorCorrectLevel.levels\n                    .map((e) => DropdownMenuItem<int>(value: e, child: Text(QrErrorCorrectLevel.getName(e))))\n                    .toList(),\n                onChanged: (value) {\n                  setState(() {\n                    errorCorrectLevel = value!;\n                  });\n                }),\n          ]),\n          const SizedBox(width: 15),\n          FilledButton.icon(\n              onPressed: () {\n                setState(() {\n                  data = inputData.text;\n                });\n              },\n              style: ButtonStyle(\n                  padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),\n                  shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                      RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),\n              icon: const Icon(Icons.qr_code, size: 18),\n              label: Text(localizations.generateQrCode, style: TextStyle(fontSize: 14))),\n          const SizedBox(width: 10),\n        ],\n      ),\n      const SizedBox(height: 10),\n      if (data != null && data?.isNotEmpty == true)\n        Column(\n          children: [\n            Row(mainAxisAlignment: MainAxisAlignment.end, children: [\n              TextButton.icon(\n                  onPressed: () async {\n                    await saveImage();\n                  },\n                  icon: const Icon(Icons.download_rounded),\n                  label: Text(localizations.saveImage)),\n              SizedBox(width: 20),\n            ]),\n            SizedBox(height: 5),\n            Center(\n                child: RepaintBoundary(\n                    key: imageKey,\n                    child: QrImageView(\n                        size: 300,\n                        data: inputData.text,\n                        backgroundColor: Colors.white,\n                        errorCorrectionLevel: errorCorrectLevel))),\n          ],\n        ),\n      SizedBox(height: 15),\n    ]);\n  }\n\n  //保存相册\n  saveImage() async {\n    if (data == null || data!.isEmpty) {\n      return;\n    }\n\n    if (Platforms.isMobile()) {\n      var imageBytes = await toImageBytes();\n      if (imageBytes == null) return;\n      String? path = await ImagePickers.saveByteDataImageToGallery(imageBytes);\n      if (path != null && mounted) {\n        FlutterToastr.show(localizations.saveSuccess, context, duration: 2, rootNavigator: true);\n      }\n      return;\n    }\n\n    String? path;\n    if (Platform.isMacOS) {\n      path = await DesktopMultiWindow.invokeMethod(0, \"saveFile\", {\"fileName\": \"qrcode.png\"});\n      WindowController.fromWindowId(widget.windowId!).show();\n    } else {\n      path = (await FilePicker.platform.saveFile(fileName: \"qrcode.png\", initialDirectory: \"~/Downloads\"));\n    }\n\n    if (path == null) return;\n\n    var imageBytes = await toImageBytes();\n    if (imageBytes == null) return;\n\n    await File(path).writeAsBytes(imageBytes);\n    if (mounted) {\n      CustomToast.success(localizations.saveSuccess).show(context);\n    }\n  }\n\n  Future<Uint8List?> toImageBytes() async {\n    RenderRepaintBoundary render = imageKey.currentContext!.findRenderObject() as RenderRepaintBoundary;\n    ui.Image image = await render.toImage();\n    ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);\n    return byteData?.buffer.asUint8List();\n  }\n}\n"
  },
  {
    "path": "lib/ui/toolbox/regexp.dart",
    "content": "/*\n * Copyright 2024 Hongen Wang All rights reserved.\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\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/ui/component/buttons.dart';\nimport 'package:proxypin/ui/component/text_field.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/utils/platform.dart';\n\n///正则表达式工具\n///@author Hongen Wang\nclass RegExpPage extends StatefulWidget {\n  final int? windowId;\n\n  const RegExpPage({super.key, this.windowId});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _RegExpPageState();\n  }\n}\n\nclass _RegExpPageState extends State<RegExpPage> {\n  var pattern = TextEditingController();\n  var input = HighlightTextEditingController();\n  var replaceText = TextEditingController();\n  String? resultInput;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    pattern.addListener(onInputChangeMatch);\n    input.addListener(onInputChangeMatch);\n\n    if (Platforms.isDesktop() && widget.windowId != null) {\n      HardwareKeyboard.instance.addHandler(onKeyEvent);\n    }\n  }\n\n  @override\n  void dispose() {\n    pattern.dispose();\n    input.dispose();\n    replaceText.dispose();\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if (widget.windowId == null) return false;\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      WindowController.fromWindowId(widget.windowId!).close();\n      return true;\n    }\n\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Color primaryColor = Theme.of(context).colorScheme.primary;\n\n    return Scaffold(\n        appBar: PreferredSize(\n            preferredSize: Size.fromHeight(50),\n            child: AppBar(\n                title: Text(localizations.regExp, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n                centerTitle: true)),\n        resizeToAvoidBottomInset: false,\n        body: ListView(padding: const EdgeInsets.all(10), children: [\n          TextField(\n            controller: pattern,\n            minLines: 1,\n            maxLines: 3,\n            onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n            decoration: decoration(context,\n                label: 'Pattern',\n                hintText: 'Enter a regular expression',\n                suffixIcon: IconButton(icon: Icon(Icons.clear), onPressed: () => pattern.clear())),\n          ),\n          const SizedBox(height: 5),\n          Wrap(\n            spacing: 8,\n            children: [\n              TextButton(\n                onPressed: () => pattern.text += r'\\d+', // Only digits\n                child: const Text('Digits'),\n              ),\n              TextButton(\n                onPressed: () => pattern.text += r'[a-zA-Z]+', // Only letters\n                child: const Text('Letters'),\n              ),\n              TextButton(\n                onPressed: () => pattern.text += r'[a-zA-Z0-9]+', // Alphanumeric\n                child: const Text('Alphanumeric'),\n              ),\n              TextButton(\n                onPressed: () => pattern.text += r'\\w+@\\w+\\.\\w+', // Email\n                child: const Text('Email'),\n              ),\n              TextButton(\n                onPressed: () => pattern.text += r'(https?|ftp)://[^\\s/$.?#].[^\\s]*', // URL\n                child: const Text('URL'),\n              ),\n              TextButton(\n                onPressed: () => pattern.text += r'\\d{4}-\\d{2}-\\d{2}', // Date (YYYY-MM-DD)\n                child: const Text('Date (YYYY-MM-DD)'),\n              ),\n            ],\n          ),\n          const SizedBox(height: 10),\n          Row(children: [\n            Align(alignment: Alignment.centerLeft, child: Text(localizations.testData)),\n            const SizedBox(width: 10),\n            if (!isMatch) Text(localizations.noChangesDetected, style: TextStyle(color: Colors.red))\n          ]),\n          const SizedBox(height: 5),\n          TextField(\n            controller: input,\n            minLines: 5,\n            maxLines: 8,\n            onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n            decoration: decoration(context, hintText: localizations.enterMatchData),\n          ),\n          const SizedBox(height: 25),\n          //输入替换文本\n          Wrap(spacing: 10, crossAxisAlignment: WrapCrossAlignment.center, children: [\n            SizedBox(\n                width: 355,\n                child: TextField(\n                  controller: replaceText,\n                  onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n                  decoration: decoration(context, label: 'Replace Text', hintText: 'Enter replacement text'),\n                )),\n            FilledButton.icon(\n                onPressed: () {\n                  if (pattern.text.isEmpty) return;\n                  setState(() {\n                    resultInput = input.text;\n                  });\n                },\n                style: Buttons.buttonStyle,\n                icon: const Icon(Icons.play_arrow_rounded),\n                label: const Text('Run')),\n            const SizedBox(width: 20),\n          ]),\n          SizedBox(height: 10),\n\n          if (resultInput != null)\n            Row(children: [\n              Text(\"Result\", style: TextStyle(fontSize: 16, color: primaryColor, fontWeight: FontWeight.w500)),\n              const SizedBox(width: 15),\n              //copy\n              IconButton(\n                  icon: Icon(Icons.copy, color: primaryColor, size: 18),\n                  onPressed: () {\n                    Clipboard.setData(ClipboardData(text: resultInput!));\n                    FlutterToastr.show(localizations.copied, context, duration: 3);\n                  }),\n            ]),\n          if (resultInput != null) SizedBox(height: 5),\n          if (resultInput != null)\n            Container(\n              padding: const EdgeInsets.all(10),\n              decoration: BoxDecoration(border: Border.all(color: Theme.of(context).colorScheme.primary, width: 1.2)),\n              child: SelectableText.rich(\n                showCursor: true,\n                TextSpan(\n                  children: _buildHighlightedText(),\n                  style: Theme.of(context).textTheme.bodyLarge,\n                ),\n              ),\n            ),\n        ]));\n  }\n\n  List<InlineSpan> _buildHighlightedText() {\n    if (resultInput == null) return [];\n\n    final spans = <InlineSpan>[];\n    int start = 0;\n\n    var text = resultInput!;\n    var regex = RegExp(pattern.text);\n    var replaceText = this.replaceText.text;\n    var matches = regex.allMatches(text);\n\n    for (var match in matches) {\n      if (start < match.start) {\n        spans.add(TextSpan(text: text.substring(start, match.start)));\n      }\n      spans.add(TextSpan(text: replaceText, style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)));\n      start = match.end;\n    }\n\n    if (start < text.length) {\n      spans.add(TextSpan(text: text.substring(start)));\n    }\n    return spans;\n  }\n\n  bool onMatch = false; //是否正在匹配\n  bool isMatch = true; //是否匹配成功\n\n  onInputChangeMatch() {\n    if (onMatch || input.highlightEnabled == false) {\n      return;\n    }\n    onMatch = true;\n\n    //高亮显示\n    Future.delayed(const Duration(milliseconds: 500), () {\n      onMatch = false;\n      if (pattern.text.isEmpty) {\n        if (isMatch) return;\n        setState(() {\n          isMatch = true;\n        });\n        return;\n      }\n\n      setState(() {\n        var match = input.highlight(pattern.text);\n        isMatch = match;\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "lib/ui/toolbox/timestamp.dart",
    "content": "import 'dart:async';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:flutter_toastr/flutter_toastr.dart';\nimport 'package:proxypin/ui/component/buttons.dart';\nimport 'package:proxypin/utils/lang.dart';\nimport 'package:proxypin/utils/platform.dart';\n\nimport '../component/text_field.dart';\n\n\n/// Timestamp page\n/// @author Hongen Wang\nclass TimestampPage extends StatefulWidget {\n  final int? windowId;\n\n  const TimestampPage({super.key, this.windowId});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _TimestampPageState();\n  }\n}\n\nclass _TimestampPageState extends State<TimestampPage> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  TextEditingController nowTimestamp = TextEditingController();\n  TextEditingController timestamp = TextEditingController();\n  TextEditingController dateTime = TextEditingController();\n\n  TextEditingController timestampOut = TextEditingController();\n  TextEditingController dateTimeOut = TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n\n    nowTimestamp.text = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();\n    timestamp.text = nowTimestamp.text;\n    dateTime.text = DateTime.now().format();\n    //定时器\n    Timer.periodic(Duration(seconds: 1), (timer) {\n      if (!mounted) {\n        timer.cancel();\n        return;\n      }\n      nowTimestamp.text = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();\n    });\n\n    if (Platforms.isDesktop() && widget.windowId != null) {\n      HardwareKeyboard.instance.addHandler(onKeyEvent);\n    }\n  }\n\n  @override\n  void dispose() {\n    nowTimestamp.dispose();\n    timestamp.dispose();\n    dateTime.dispose();\n    timestampOut.dispose();\n    dateTimeOut.dispose();\n    HardwareKeyboard.instance.removeHandler(onKeyEvent);\n    super.dispose();\n  }\n\n  bool onKeyEvent(KeyEvent event) {\n    if (widget.windowId == null) return false;\n    if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&\n        event.logicalKey == LogicalKeyboardKey.keyW) {\n      HardwareKeyboard.instance.removeHandler(onKeyEvent);\n      WindowController.fromWindowId(widget.windowId!).close();\n      return true;\n    }\n\n    return false;\n  }\n\n  TextStyle? get textStyle => Theme.of(context).textTheme.titleMedium;\n\n  bool get isCN => Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text(localizations.timestamp, style: TextStyle(fontSize: 16)), centerTitle: true),\n      body: ListView(\n        padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),\n        children: [\n          Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [\n            Text('${localizations.nowTimestamp}:', style: textStyle),\n            const SizedBox(width: 6),\n            SizedBox(\n                width: 100,\n                child: TextField(\n                    controller: nowTimestamp, readOnly: true, decoration: InputDecoration(border: InputBorder.none))),\n            IconButton(\n                icon: Icon(Icons.copy, size: 18),\n                onPressed: () {\n                  Clipboard.setData(ClipboardData(text: nowTimestamp.text));\n                  FlutterToastr.show(localizations.copied, context);\n                })\n          ]),\n          SizedBox(height: 15),\n          if (Platforms.isDesktop())\n            Wrap(spacing: 10.0, runSpacing: 10.0, crossAxisAlignment: WrapCrossAlignment.center, children: [\n              timestampLabel(),\n              SizedBox(width: 220, child: timestampField()),\n              timestampButton(),\n              SizedBox(width: 210, child: timestampOutField()),\n            ]),\n          if (Platforms.isMobile())\n            Row(children: [\n              timestampLabel(),\n              SizedBox(width: 8),\n              Expanded(\n                  child: Column(children: [\n                timestampField(),\n                SizedBox(height: 5),\n                Row(\n                    mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                    children: [timestampButton(), timestampOutCopyButton()]),\n                SizedBox(height: 5),\n                timestampOutField()\n              ])),\n            ]),\n          SizedBox(height: 35),\n          if (Platforms.isDesktop())\n            Wrap(spacing: 10.0, runSpacing: 10.0, crossAxisAlignment: WrapCrossAlignment.center, children: [\n              timeLabel(),\n              SizedBox(width: 220, child: timeField()),\n              timeButton(),\n              SizedBox(width: 210, child: timeOutField())\n            ]),\n          if (Platforms.isMobile())\n            Row(children: [\n              timeLabel(),\n              SizedBox(width: 8),\n              Expanded(\n                  child: Column(children: [\n                timeField(),\n                SizedBox(height: 5),\n                Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [timeButton(), timeOutCopyButton()]),\n                SizedBox(height: 5),\n                timeOutField()\n              ])),\n            ]),\n        ],\n      ),\n    );\n  }\n\n  Widget timestampLabel() {\n    return SizedBox(width: isCN ? 60 : 93, child: Text('${localizations.timestamp}:', style: textStyle));\n  }\n\n  Widget timestampButton() {\n    return SizedBox(\n        height: 40,\n        child: FilledButton.icon(\n            icon: Icon(Icons.play_arrow_rounded, size: 20),\n            style: Buttons.buttonStyle,\n            label: Text(localizations.convert),\n            onPressed: () => timestampConvert(timestamp.text)));\n  }\n\n  Widget timestampField() {\n    return TextFormField(\n        controller: timestamp,\n        onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n        decoration: decoration(context,\n            hintText: 'timestamp',\n            suffixIcon: IconButton(icon: Icon(Icons.clear, size: 20), onPressed: () => timestamp.clear())));\n  }\n\n  Widget timestampOutField() {\n    return TextFormField(\n        controller: timestampOut, readOnly: true, decoration: InputDecoration(border: OutlineInputBorder()));\n  }\n\n  Widget timestampOutCopyButton() {\n    return IconButton(\n        icon: Icon(Icons.copy, size: 22),\n        onPressed: () {\n          if (timestampOut.text.isEmpty) return;\n          Clipboard.setData(ClipboardData(text: timestampOut.text));\n          FlutterToastr.show(localizations.copied, context);\n        });\n  }\n\n  Widget timeLabel() {\n    return SizedBox(width: isCN ? 60 : 93, child: Text('${localizations.time}:', style: textStyle));\n  }\n\n  Widget timeField() {\n    return TextFormField(\n        controller: dateTime,\n        onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n        decoration: decoration(context,\n            hintText: 'yyyy-MM-dd HH:mm:ss',\n            suffixIcon: IconButton(icon: Icon(Icons.clear, size: 20), onPressed: () => dateTime.clear())));\n  }\n\n  Widget timeButton() {\n    return SizedBox(\n        height: 40,\n        child: FilledButton.icon(\n            icon: Icon(Icons.play_arrow_rounded, size: 20),\n            style: Buttons.buttonStyle,\n            label: Text(localizations.convert),\n            onPressed: () => timeConvert(dateTime.text)));\n  }\n\n  Widget timeOutField() {\n    return TextFormField(\n        controller: dateTimeOut, readOnly: true, decoration: InputDecoration(border: OutlineInputBorder()));\n  }\n\n  Widget timeOutCopyButton() {\n    return IconButton(\n        icon: Icon(Icons.copy, size: 22),\n        onPressed: () {\n          if (dateTimeOut.text.isEmpty) return;\n          Clipboard.setData(ClipboardData(text: dateTimeOut.text));\n          FlutterToastr.show(localizations.copied, context);\n        });\n  }\n\n  timestampConvert(String timestamp) {\n    if (timestamp.isEmpty) return;\n    try {\n      if (timestamp.length == 13) {\n        timestampOut.text = DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp)).format();\n        return;\n      }\n\n      if (timestamp.length == 10) {\n        timestampOut.text = DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp) * 1000).format();\n        return;\n      }\n      FlutterToastr.show('Invalid timestamp', context);\n    } catch (e) {\n      FlutterToastr.show('Invalid timestamp', context);\n    }\n  }\n\n  timeConvert(String dateTime) {\n    if (dateTime.isEmpty) return;\n    try {\n      var date = DateTime.parse(dateTime);\n      dateTimeOut.text = (date.millisecondsSinceEpoch ~/ 1000).toString();\n    } catch (e) {\n      FlutterToastr.show('Invalid date time', context);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/toolbox/toolbox.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:proxypin/network/bin/server.dart';\nimport 'package:proxypin/ui/component/multi_window.dart';\nimport 'package:proxypin/ui/mobile/request/request_editor.dart';\nimport 'package:proxypin/ui/toolbox/qr_code_page.dart';\nimport 'package:proxypin/ui/toolbox/regexp.dart';\nimport 'package:proxypin/ui/toolbox/timestamp.dart';\nimport 'package:proxypin/utils/platform.dart';\nimport 'package:window_manager/window_manager.dart';\n\nimport 'aes_page.dart';\nimport 'cert_hash.dart';\nimport 'encoder.dart';\nimport 'js_run.dart';\nimport 'websocket_request.dart';\n\nclass Toolbox extends StatefulWidget {\n  final ProxyServer? proxyServer;\n\n  const Toolbox({super.key, this.proxyServer});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _ToolboxState();\n  }\n}\n\nclass _ToolboxState extends State<Toolbox> {\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  Widget build(BuildContext context) {\n    return IconTheme(\n        data: IconTheme.of(context).copyWith(color: IconTheme.of(context).color?.withValues(alpha: 0.65), size: 22),\n        child: SingleChildScrollView(\n            child: Container(\n          padding: const EdgeInsets.all(10),\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.start,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              // Top quick actions\n              Wrap(\n                spacing: 6,\n                children: [\n                  IconText(\n                    icon: Icons.http,\n                    text: \"HTTP\",\n                    onTap: httpRequest,\n                    tooltip: localizations.httpRequest,\n                  ),\n                  IconText(\n                      onTap: () async {\n                        if (Platforms.isMobile()) {\n                          Navigator.of(context)\n                              .push(MaterialPageRoute(builder: (context) => const WebSocketRequestPage()));\n                          return;\n                        }\n                        MultiWindow.openWindow('WebSocket', 'WebSocketRequestPage', size: const Size(800, 600));\n                      },\n                      icon: Icons.wifi_tethering,\n                      text: 'WebSocket',\n                      tooltip: 'WebSocket'),\n                  IconText(\n                    icon: Icons.javascript,\n                    text: 'JavaScript',\n                    tooltip: 'JavaScript',\n                    onTap: () async {\n                      if (Platforms.isMobile()) {\n                        Navigator.of(context).push(MaterialPageRoute(builder: (context) => const JavaScript()));\n                        return;\n                      }\n\n                      var size = MediaQuery.of(context).size;\n                      var ratio = 1.0;\n                      if (Platform.isWindows) {\n                        ratio = WindowManager.instance.getDevicePixelRatio();\n                      }\n\n                      final window = await DesktopMultiWindow.createWindow(jsonEncode(\n                        {'name': 'JavaScript'},\n                      ));\n                      window.setTitle('JavaScript');\n                      window\n                        ..setFrame(const Offset(100, 100) & Size(960 * ratio, size.height * ratio))\n                        ..center()\n                        ..show();\n                    },\n                  ),\n                ],\n              ),\n              const Divider(thickness: 0.3),\n              Text(localizations.encode, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n              Wrap(\n                spacing: 6,\n                children: [\n                  IconText(\n                    onTap: () => encodeWindow(EncoderType.url, context),\n                    icon: Icons.link,\n                    text: 'URL',\n                    tooltip: 'URL Encode/Decode',\n                  ),\n                  IconText(\n                    onTap: () => encodeWindow(EncoderType.base64, context),\n                    icon: Icons.format_bold_outlined,\n                    text: 'Base64',\n                    tooltip: 'Base64 Encode/Decode',\n                  ),\n                  IconText(\n                    onTap: () => encodeWindow(EncoderType.unicode, context),\n                    icon: Icons.format_underline_outlined,\n                    text: 'Unicode',\n                    tooltip: 'Unicode Encode/Decode',\n                  ),\n                  IconText(\n                    onTap: () => encodeWindow(EncoderType.md5, context),\n                    icon: Icons.tag_outlined,\n                    text: 'MD5',\n                    tooltip: 'MD5 Hash',\n                  ),\n                ],\n              ),\n              const Divider(thickness: 0.3),\n              Text(localizations.cipher, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n              Wrap(\n                spacing: 6,\n                children: [\n                  IconText(\n                    onTap: () {\n                      if (Platforms.isMobile()) {\n                        Navigator.of(context).push(MaterialPageRoute(builder: (context) => const AesPage()));\n                        return;\n                      }\n                      MultiWindow.openWindow(\"AES\", \"AesPage\", size: const Size(700, 672));\n                    },\n                    icon: Icons.enhanced_encryption_outlined,\n                    text: 'AES',\n                    tooltip: 'AES Encrypt/Decrypt',\n                  ),\n                ],\n              ),\n              const Divider(thickness: 0.3),\n              Text(localizations.other, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),\n              Wrap(\n                spacing: 6,\n                children: [\n                  IconText(\n                      onTap: () async {\n                        if (Platforms.isMobile()) {\n                          Navigator.of(context).push(MaterialPageRoute(builder: (context) => const TimestampPage()));\n                          return;\n                        }\n\n                        MultiWindow.openWindow(localizations.timestamp, 'TimestampPage', size: const Size(700, 350));\n                      },\n                      icon: Icons.av_timer,\n                      text: localizations.timestamp,\n                      tooltip: localizations.timestamp),\n                  IconText(\n                      onTap: () async {\n                        if (Platforms.isMobile()) {\n                          Navigator.of(context).push(MaterialPageRoute(builder: (context) => const CertHashPage()));\n                          return;\n                        }\n                        MultiWindow.openWindow(localizations.certHashName, 'CertHashPage');\n                      },\n                      icon: Icons.key_outlined,\n                      text: localizations.certHashName,\n                      tooltip: localizations.certHashName),\n                  IconText(\n                      onTap: () async {\n                        if (Platforms.isMobile()) {\n                          Navigator.of(context).push(MaterialPageRoute(builder: (context) => const RegExpPage()));\n                          return;\n                        }\n                        MultiWindow.openWindow(localizations.regExp, 'RegExpPage', size: const Size(800, 720));\n                      },\n                      icon: Icons.code,\n                      text: localizations.regExp,\n                      tooltip: localizations.regExp),\n                  IconText(\n                      onTap: () async {\n                        if (Platforms.isMobile()) {\n                          Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QrCodePage()));\n                          return;\n                        }\n                        MultiWindow.openWindow(localizations.qrCode, 'QrCodePage');\n                      },\n                      icon: Icons.qr_code_2,\n                      text: localizations.qrCode,\n                      tooltip: localizations.qrCode),\n                ],\n              ),\n            ],\n          ),\n        )));\n  }\n\n  Future<void> httpRequest() async {\n    if (Platforms.isMobile()) {\n      Navigator.of(context)\n          .push(MaterialPageRoute(builder: (context) => MobileRequestEditor(proxyServer: widget.proxyServer)));\n      return;\n    }\n\n    var size = MediaQuery.of(context).size;\n    var ratio = 1.0;\n    if (Platform.isWindows) {\n      ratio = WindowManager.instance.getDevicePixelRatio();\n    }\n\n    final window = await DesktopMultiWindow.createWindow(jsonEncode(\n      {'name': 'RequestEditor'},\n    ));\n    window.setTitle(localizations.httpRequest);\n    window\n      ..setFrame(const Offset(100, 100) & Size(960 * ratio, size.height * ratio))\n      ..center()\n      ..show();\n  }\n}\n\nclass IconText extends StatelessWidget {\n  final IconData icon;\n  final String text;\n  final String? tooltip;\n\n  /// Called when the user taps this part of the material.\n  final GestureTapCallback? onTap;\n\n  const IconText({super.key, required this.icon, required this.text, this.onTap, this.tooltip});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    final label = text;\n    return Tooltip(\n      message: tooltip ?? label,\n      waitDuration: const Duration(milliseconds: 500),\n      child: Material(\n        type: MaterialType.transparency,\n        child: InkWell(\n          onTap: onTap,\n          borderRadius: BorderRadius.circular(10),\n          hoverColor: theme.colorScheme.primary.withValues(alpha: 0.06),\n          splashColor: theme.colorScheme.primary.withValues(alpha: 0.12),\n          child: Container(\n            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),\n            constraints: const BoxConstraints(minWidth: 92),\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                Icon(icon),\n                const SizedBox(height: 6),\n                Text(label, style: const TextStyle(fontSize: 14)),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/ui/toolbox/websocket_request.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:desktop_multi_window/desktop_multi_window.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:proxypin/l10n/app_localizations.dart';\nimport 'package:file_picker/file_picker.dart';\n\nimport '../../utils/platform.dart';\nimport '../component/app_dialog.dart';\nimport '../component/json/json_text.dart';\nimport '../component/json/json_viewer.dart';\nimport '../component/json/theme.dart';\n\n/// Simple WebSocket request page: connect to ws/wss URL, send text, view messages\nclass WebSocketRequestPage extends StatefulWidget {\n  final int? windowId; // optional for desktop multi-window\n  const WebSocketRequestPage({super.key, this.windowId});\n\n  @override\n  State<WebSocketRequestPage> createState() => _WebSocketRequestPageState();\n}\n\nclass _WebSocketRequestPageState extends State<WebSocketRequestPage> {\n  final ScrollController _scrollController = ScrollController();\n  bool _scrollScheduled = false;\n\n  // whether the view is considered near the bottom. If false, auto-scroll is disabled\n  bool _isNearBottom = true;\n  static const double _autoScrollThreshold = 150.0;\n\n  // key for the currently last message widget so we can ensureVisible it\n  final GlobalKey _lastMessageKey = GlobalKey();\n\n  // key for the input bar so we can position the jump button just above it\n  final GlobalKey _inputBarKey = GlobalKey();\n  final TextEditingController _urlController = TextEditingController(text: 'ws://');\n  final TextEditingController _sendController = TextEditingController();\n  final List<_WsMessage> _messages = [];\n\n  WebSocket? _socket;\n  StreamSubscription? _sub;\n  bool _connecting = false;\n  bool _connected = false;\n\n  AppLocalizations get localizations => AppLocalizations.of(context)!;\n\n  @override\n  void initState() {\n    super.initState();\n    // listen to scroll changes to determine whether we should auto-scroll\n    _scrollController.addListener(() {\n      if (!_scrollController.hasClients) return;\n      final max = _scrollController.position.maxScrollExtent;\n      final offset = _scrollController.offset;\n      final near = (max - offset) <= _autoScrollThreshold;\n      if (near != _isNearBottom) {\n        setState(() {\n          _isNearBottom = near;\n        });\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    _sub?.cancel();\n    _socket?.close();\n    _scrollController.dispose();\n    _urlController.dispose();\n    _sendController.dispose();\n    super.dispose();\n  }\n\n  Future<void> _connect() async {\n    final url = _urlController.text.trim();\n    if (url.isEmpty || !(url.startsWith('ws://') || url.startsWith('wss://'))) {\n      CustomToast.error('Invalid URL').show(context);\n      return;\n    }\n    setState(() {\n      _connecting = true;\n    });\n    try {\n      final socket = await WebSocket.connect(url);\n      _socket = socket;\n      _connected = true;\n      _connecting = false;\n      _listen();\n      setState(() {});\n      _addSys('Connected');\n    } catch (e) {\n      _connecting = false;\n      _connected = false;\n      setState(() {});\n      _addSys('Connect failed: $e');\n    }\n  }\n\n  void _listen() {\n    _sub?.cancel();\n    _sub = _socket?.listen((data) {\n      // data can be String or List<int>\n      if (data is String) {\n        _messages.add(_WsMessage(false, utf8.encode(data), false, time: DateTime.now()));\n      } else if (data is List<int>) {\n        _messages.add(_WsMessage(false, List<int>.from(data), true, time: DateTime.now()));\n      } else {\n        _messages.add(_WsMessage(false, utf8.encode('$data'), false, time: DateTime.now()));\n      }\n      setState(() {});\n      _scheduleScroll();\n    }, onError: (error) {\n      _addSys('Error: $error');\n    }, onDone: () {\n      _connected = false;\n      setState(() {});\n      _addSys('Closed');\n    });\n  }\n\n  Future<void> _disconnect() async {\n    try {\n      await _socket?.close();\n    } catch (_) {}\n    _connected = false;\n    setState(() {});\n    _addSys('Disconnected');\n  }\n\n  void _sendText() {\n    final text = _sendController.text.trim();\n    if (!_connected || text.isEmpty) return;\n    _socket?.add(text);\n    _messages.add(_WsMessage(true, utf8.encode(text), false, time: DateTime.now()));\n    _sendController.clear();\n    setState(() {});\n    _scheduleScroll();\n  }\n\n  Future<void> _sendFile() async {\n    if (!_connected) return;\n    try {\n      String? path;\n      if (Platforms.isMobile()) {\n        final result = await FilePicker.platform.pickFiles(allowMultiple: false);\n        if (result == null || result.files.isEmpty) return;\n        path = result.files.single.path;\n      } else {\n        path = path = await DesktopMultiWindow.invokeMethod(0, \"pickFiles\");\n        if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show();\n      }\n      if (path == null) return;\n      final file = File(path);\n      final bytes = await file.readAsBytes();\n      if (bytes.isEmpty) return;\n      _socket?.add(bytes);\n      _messages.add(_WsMessage(true, bytes.toList(), true, time: DateTime.now()));\n      setState(() {});\n      _scheduleScroll();\n      if (mounted) {\n        CustomToast.success(AppLocalizations.of(context)!.send).show(context);\n      }\n    } catch (e) {\n      if (mounted) {\n        CustomToast.error('Send file failed: $e').show(context);\n      }\n    }\n  }\n\n  void _addSys(String msg) {\n    _messages.add(_WsMessage.system(msg));\n    setState(() {});\n    _scheduleScroll();\n  }\n\n  void _clearMessages() {\n    if (_messages.isEmpty) return;\n    setState(() {\n      _messages.clear();\n    });\n  }\n\n  String _formatTime(DateTime dt) {\n    final d = dt.toLocal();\n    String two(int n) => n.toString().padLeft(2, '0');\n    return '${two(d.hour)}:${two(d.minute)}:${two(d.second)}';\n  }\n\n  String _formatSize(int bytes) {\n    const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n    double size = bytes.toDouble();\n    int unitIndex = 0;\n    while (size >= 1024 && unitIndex < units.length - 1) {\n      size /= 1024;\n      unitIndex++;\n    }\n    return '${size.toStringAsFixed(size < 10 ? 2 : 1)} ${units[unitIndex]}';\n  }\n\n  void _scheduleScroll() {\n    // only auto-scroll when the user is already near the bottom\n    if (!_isNearBottom) return;\n    if (_scrollScheduled) return;\n    _scrollScheduled = true;\n    WidgetsBinding.instance.addPostFrameCallback((_) async {\n      _scrollScheduled = false;\n      // give layout a bit more time to settle (helps when many messages are added quickly)\n      await Future.delayed(const Duration(milliseconds: 120));\n      // prefer Scrollable.ensureVisible on the last message for more natural behavior\n      try {\n        final ctx = _lastMessageKey.currentContext;\n        if (ctx != null) {\n          // use alignment slightly above bottom to avoid being hidden by input controls\n          await Scrollable.ensureVisible(ctx,\n              duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, alignment: 0.9);\n          return;\n        }\n      } catch (_) {}\n      await _animateToBottom();\n    });\n  }\n\n  Future<void> _animateToBottom() async {\n    if (!_scrollController.hasClients) return;\n    final max = _scrollController.position.maxScrollExtent;\n    try {\n      await _scrollController.animateTo(max - 10, duration: Duration(milliseconds: 350), curve: Curves.easeInOut);\n    } catch (_) {\n      try {\n        _scrollController.jumpTo(_scrollController.position.maxScrollExtent);\n      } catch (_) {}\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    // Use a Stack so we can place a custom-styled \"jump to latest\" button\n    return Scaffold(\n      appBar: AppBar(\n          title: Text('WebSocket', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),\n          centerTitle: true,\n          actions: [\n            IconButton(\n              tooltip: 'Clear messages',\n              icon: const Icon(Icons.delete),\n              onPressed: () => _clearMessages(),\n            ),\n            SizedBox(width: 8),\n          ]),\n      body: Stack(children: [\n        // main content\n        Column(children: [\n          Padding(\n            padding: const EdgeInsets.all(10),\n            child: Row(children: [\n              Expanded(\n                child: TextField(\n                  controller: _urlController,\n                  decoration: InputDecoration(labelText: 'ws(s)://', border: const OutlineInputBorder(), isDense: true),\n                ),\n              ),\n              const SizedBox(width: 8),\n              ElevatedButton(\n                  onPressed: _connecting ? null : (_connected ? _disconnect : _connect),\n                  child: Text(_connected ? localizations.disconnect : localizations.connect)),\n            ]),\n          ),\n          const Divider(height: 0),\n          Expanded(child: _messageList(theme)),\n          const Divider(height: 0, thickness: 0.2),\n          Padding(\n            key: _inputBarKey,\n            padding: const EdgeInsets.all(10),\n            child: Row(children: [\n              IconButton(\n                icon: Icon(Icons.attach_file, color: theme.colorScheme.primary),\n                onPressed: _connected ? _sendFile : null,\n              ),\n\n              const SizedBox(width: 4),\n              Expanded(\n                child: Shortcuts(\n                  shortcuts: {\n                    // Enter sends\n                    SingleActivator(LogicalKeyboardKey.enter): const _SendIntent(),\n                    // Ctrl+Enter inserts newline (also meta/cmd on macOS)\n                    SingleActivator(LogicalKeyboardKey.enter, control: true): const _InsertNewlineIntent(),\n                    SingleActivator(LogicalKeyboardKey.enter, meta: true): const _InsertNewlineIntent(),\n                  },\n                  child: Actions(\n                    actions: {\n                      _SendIntent: CallbackAction<_SendIntent>(onInvoke: (intent) {\n                        if (_connected) _sendText();\n                        return null;\n                      }),\n                      _InsertNewlineIntent: CallbackAction<_InsertNewlineIntent>(onInvoke: (intent) {\n                        // Insert a newline at the current cursor position\n                        final controller = _sendController;\n                        final text = controller.text;\n                        final sel = controller.selection;\n                        final start = sel.start >= 0 ? sel.start : text.length;\n                        final end = sel.end >= 0 ? sel.end : text.length;\n                        final newText = text.replaceRange(start, end, '\\n');\n                        controller.value = TextEditingValue(\n                          text: newText,\n                          selection: TextSelection.collapsed(offset: start + 1),\n                        );\n                        return null;\n                      }),\n                    },\n                    child: TextField(\n                      controller: _sendController,\n                      minLines: 1,\n                      maxLines: 4,\n                      keyboardType: TextInputType.multiline,\n                      textInputAction: TextInputAction.newline,\n                      decoration: InputDecoration(\n                        labelText: localizations.requestBody,\n                        border: const OutlineInputBorder(),\n                        isDense: true,\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n              const SizedBox(width: 8),\n\n              // Telegram-style circular send button\n              Tooltip(\n                message: localizations.send,\n                child: Opacity(\n                  opacity: _connected ? 1.0 : 0.5,\n                  child: InkWell(\n                    onTap: _connected ? _sendText : null,\n                    borderRadius: BorderRadius.circular(22),\n                    child: Container(\n                      width: 34,\n                      height: 34,\n                      decoration: BoxDecoration(\n                        color: Theme.of(context).colorScheme.primary,\n                        shape: BoxShape.circle,\n                        boxShadow: [\n                          BoxShadow(\n                              color: Colors.black.withValues(alpha: 0.2), blurRadius: 6, offset: const Offset(0, 3))\n                        ],\n                      ),\n                      child: const Icon(Icons.send, color: Colors.white, size: 20),\n                    ),\n                  ),\n                ),\n              ),\n            ]),\n          )\n        ]),\n        // positioned jump-to-latest button (custom style). It is placed above the input area and above\n        // the keyboard by using MediaQuery.viewInsets.bottom as extra offset.\n        if (!_isNearBottom)\n          // pill-shaped jump button placed just above the input bar, aligned to the right\n          Positioned(\n            right: 16,\n            bottom: () {\n              final inputContext = _inputBarKey.currentContext;\n              final viewInsets = MediaQuery.of(context).viewInsets.bottom;\n              if (inputContext != null) {\n                final renderBox = inputContext.findRenderObject() as RenderBox?;\n                if (renderBox != null) {\n                  final h = renderBox.size.height;\n                  return (h + 12.0 + viewInsets);\n                }\n              }\n              return 80.0 + viewInsets;\n            }(),\n            child: AnimatedOpacity(\n              duration: const Duration(milliseconds: 220),\n              opacity: !_isNearBottom ? 1.0 : 0.0,\n              child: Semantics(\n                label: 'Jump to latest messages',\n                button: true,\n                child: Material(\n                  elevation: 10,\n                  color: Colors.transparent,\n                  child: InkWell(\n                    onTap: () async {\n                      await _animateToBottom();\n                      if (!_isNearBottom) {\n                        setState(() {\n                          _isNearBottom = true;\n                        });\n                      }\n                    },\n                    borderRadius: BorderRadius.circular(20.0),\n                    child: Container(\n                      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),\n                      decoration: BoxDecoration(\n                        color: Theme.of(context).colorScheme.primary,\n                        borderRadius: BorderRadius.circular(20.0),\n                        boxShadow: [\n                          BoxShadow(\n                              color: Colors.black.withValues(alpha: 0.24), blurRadius: 8, offset: const Offset(0, 4))\n                        ],\n                      ),\n                      child: const Icon(Icons.arrow_downward, color: Colors.white, size: 18),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n      ]),\n    );\n  }\n\n  Widget _messageList(ThemeData theme) {\n    // Add extra bottom padding when the jump button is visible to avoid covering content\n    double baseBottom = 10;\n    double extraBottom = 0;\n    if (!_isNearBottom) {\n      final inputContext = _inputBarKey.currentContext;\n      if (inputContext != null) {\n        final renderBox = inputContext.findRenderObject() as RenderBox?;\n        if (renderBox != null) {\n          // button height ~ 32 (pill) + margin 16; ensure some extra spacing for safe area\n          extraBottom = 48; // conservative spacing\n        }\n      } else {\n        extraBottom = 48;\n      }\n    }\n    return ListView.separated(\n      controller: _scrollController,\n      padding: EdgeInsets.fromLTRB(10, 10, 10, baseBottom + extraBottom),\n      itemBuilder: (context, index) {\n        final m = _messages[index];\n        if (m.isSystem) {\n          return Center(\n              child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              SelectionContainer.disabled(\n                  child: Text(_formatTime(m.time), style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey))),\n              const SizedBox(height: 4),\n              Text(m.textPreview(), style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)),\n            ],\n          ));\n        }\n        final displayOnLeft = !m.isClient;\n        final avatar = CircleAvatar(\n          backgroundColor: m.isClient ? Colors.green : Colors.blue,\n          child: Text(m.isClient ? 'C' : 'S', style: const TextStyle(color: Colors.white)),\n        );\n        final bubbleText = m.isBinary ? '[binary ${_formatSize(m.bytes.length)}]' : m.textPreview();\n        final bubble = Container(\n          padding: const EdgeInsets.all(8),\n          decoration: BoxDecoration(\n            color: displayOnLeft ? Colors.green.withValues(alpha: 0.2) : Colors.blue.withValues(alpha: 0.2),\n            borderRadius: BorderRadius.circular(8),\n          ),\n          child: SelectableText(bubbleText),\n        );\n        final previewButton = IconButton(\n          onPressed: () {\n            showDialog(context: context, builder: (context) => _PreviewDialog(bytes: m.bytes));\n          },\n          icon: Icon(Icons.expand_more, color: ColorScheme.of(context).primary),\n        );\n        // attach key to the last message so we can ensureVisible it\n        final widgetKey = index == _messages.length - 1 ? _lastMessageKey : null;\n        return Padding(\n          padding: const EdgeInsets.only(bottom: 8),\n          key: widgetKey,\n          child: Row(\n            mainAxisAlignment: displayOnLeft ? MainAxisAlignment.start : MainAxisAlignment.end,\n            children: [\n              if (displayOnLeft) avatar,\n              const SizedBox(width: 8),\n              Flexible(\n                child: Column(\n                  crossAxisAlignment: displayOnLeft ? CrossAxisAlignment.start : CrossAxisAlignment.end,\n                  children: [\n                    SelectionContainer.disabled(\n                        child:\n                            Text(_formatTime(m.time), style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey))),\n                    const SizedBox(height: 4),\n                    Row(mainAxisSize: MainAxisSize.min, children: [\n                      if (!displayOnLeft) previewButton,\n                      Flexible(child: bubble),\n                      if (displayOnLeft) previewButton,\n                    ]),\n                  ],\n                ),\n              ),\n              if (!displayOnLeft) const SizedBox(width: 8),\n              if (!displayOnLeft) avatar,\n            ],\n          ),\n        );\n      },\n      separatorBuilder: (context, index) => const SizedBox(height: 8),\n      itemCount: _messages.length,\n    );\n  }\n}\n\nclass _WsMessage {\n  final bool isClient;\n  final List<int> bytes;\n  final bool isBinary;\n  final DateTime time;\n\n  _WsMessage(this.isClient, this.bytes, this.isBinary, {DateTime? time}) : time = time ?? DateTime.now();\n\n  bool get isSystem => bytes.isEmpty;\n\n  String textPreview() {\n    if (isSystem) return utf8.decode(bytes);\n    return utf8.decode(bytes);\n  }\n\n  @override\n  String toString() {\n    return 'Message(isClient: $isClient, bytes: $bytes, isBinary: $isBinary, time: $time)';\n  }\n\n  factory _WsMessage.system(String text) {\n    return _WsMessage(false, utf8.encode(text), false);\n  }\n}\n\nclass _PreviewDialog extends StatefulWidget {\n  final List<int> bytes;\n\n  const _PreviewDialog({required this.bytes});\n\n  @override\n  State<_PreviewDialog> createState() => _PreviewDialogState();\n}\n\nclass _PreviewDialogState extends State<_PreviewDialog> {\n  int tabIndex = 0; // 0: HEX, 1: TEXT\n\n  @override\n  Widget build(BuildContext context) {\n    var tabs = [\n      if (isJsonText(widget.bytes)) const Tab(text: \"JSON Text\"),\n      if (isJsonText(widget.bytes)) const Tab(text: \"JSON\"),\n      const Tab(text: \"TEXT\"),\n      const Tab(text: \"HEX\"),\n    ];\n\n    return AlertDialog(\n      content: SizedBox(\n        width: MediaQuery.of(context).size.width * 0.8,\n        height: MediaQuery.of(context).size.height * 0.6,\n        child: DefaultTabController(\n          length: tabs.length,\n          initialIndex: tabIndex,\n          child: Column(mainAxisSize: MainAxisSize.min, children: [\n            TabBar(\n              tabs: tabs,\n              onTap: (index) {\n                setState(() {\n                  tabIndex = index;\n                });\n              },\n            ),\n            Expanded(\n              child: TabBarView(children: [\n                if (isJsonText(widget.bytes))\n                  SingleChildScrollView(padding: const EdgeInsets.all(8.0), child: jsonText()),\n                if (isJsonText(widget.bytes))\n                  SingleChildScrollView(padding: const EdgeInsets.all(8.0), child: jsonView()),\n                // TEXT\n                SingleChildScrollView(\n                  padding: const EdgeInsets.all(8),\n                  child: SelectableText(safeTextPreview(widget.bytes)),\n                ),\n\n                // HEX\n                SingleChildScrollView(\n                  padding: const EdgeInsets.all(8),\n                  child: SelectableText(widget.bytes.map(intToHex).join(\" \")),\n                ),\n              ]),\n            ),\n          ]),\n        ),\n      ),\n      actions: [\n        TextButton(\n            onPressed: () => Navigator.of(context).pop(),\n            child: Text(MaterialLocalizations.of(context).closeButtonLabel))\n      ],\n    );\n  }\n\n  Widget jsonText() {\n    String body = utf8.decode(widget.bytes, allowMalformed: true);\n    dynamic jsonData;\n    try {\n      jsonData = json.decode(body);\n    } catch (e) {\n      jsonData = null;\n    }\n\n    if (jsonData == null) {\n      return SelectableText(safeTextPreview(widget.bytes));\n    }\n\n    return JsonText(json: jsonData, indent: '    ', colorTheme: ColorTheme.of(context));\n  }\n\n  Widget jsonView() {\n    String body = utf8.decode(widget.bytes, allowMalformed: true);\n    dynamic jsonData;\n    try {\n      jsonData = json.decode(body);\n    } catch (e) {\n      jsonData = null;\n    }\n\n    if (jsonData == null) {\n      return SelectableText(safeTextPreview(widget.bytes));\n    }\n\n    return JsonViewer(json.decode(body), colorTheme: ColorTheme.of(context));\n  }\n\n  //判断是否是json格式\n  bool isJsonText(List<int> bytes) {\n    return bytes.isNotEmpty && (bytes[0] == 0x7B || bytes[0] == 0x5B);\n  }\n\n  String intToHex(int b) => b.toRadixString(16).padLeft(2, '0');\n\n  /// Decode bytes to string, non-printable as '.'\n  String safeTextPreview(List<int> bytes) {\n    try {\n      return utf8.decode(bytes);\n    } catch (_) {\n      return bytes.map((b) => b >= 32 && b <= 126 ? String.fromCharCode(b) : '.').join();\n    }\n  }\n}\n\nclass _SendIntent extends Intent {\n  const _SendIntent();\n}\n\nclass _InsertNewlineIntent extends Intent {\n  const _InsertNewlineIntent();\n}\n"
  },
  {
    "path": "lib/utils/aes.dart",
    "content": "import 'dart:convert';\nimport 'dart:typed_data';\n\nimport 'package:pointycastle/export.dart';\n\nclass AesUtils {\n  static Uint8List encrypt(Uint8List input,\n      {required String key, required int keyLength, required String mode, required String padding, String? iv}) {\n    return _process(input, true,\n        key: key, keyLength: keyLength, mode: mode, padding: padding, iv: iv);\n  }\n\n  static Uint8List decrypt(Uint8List input,\n      {required String key, required int keyLength, required String mode, required String padding, String? iv}) {\n    var data = _process(input, false,\n        key: key, keyLength: keyLength, mode: mode, padding: padding, iv: iv);\n    // 移除填充零字节（仅 ZeroPadding 场景）\n    if (padding == 'ZeroPadding') {\n      int lastNonZeroIndex = data.lastIndexWhere((byte) => byte != 0);\n      if (lastNonZeroIndex < 0) return Uint8List(0);\n      data = data.sublist(0, lastNonZeroIndex + 1);\n    }\n    return data;\n  }\n\n  // Refactored process method (renamed to _process and split into helpers)\n  static Uint8List _process(Uint8List input, bool isEncrypt,\n      {required String key, required int keyLength, required String mode, required String padding, String? iv}) {\n    final int keySize = keyLength ~/ 8;\n\n    // Build key bytes: support 'base64:' prefix or plain text\n    final keyBytes = _buildKeyBytes(key, keySize);\n\n    // If CBC mode, prepare IV bytes\n    Uint8List? ivBytes;\n    if (mode == 'CBC') {\n      if (iv == null) {\n        throw ArgumentError.value(iv, 'iv', 'IV is required for CBC mode');\n      }\n      ivBytes = _buildIvBytes(iv);\n      // Ensure IV is block-size (16) length\n      final blockSize = 16;\n      if (ivBytes.length < blockSize) {\n        final tmp = Uint8List(blockSize);\n        tmp.setRange(0, ivBytes.length, ivBytes);\n        ivBytes = tmp;\n      } else if (ivBytes.length > blockSize) {\n        ivBytes = ivBytes.sublist(0, blockSize);\n      }\n    }\n\n    final aesEngine = AESEngine();\n\n    // When encrypting with ZeroPadding, pad input to block size\n    if (isEncrypt && padding == 'ZeroPadding') {\n      input = _padZeroForEncrypt(input, aesEngine.blockSize);\n    }\n\n    // PKCS7 path\n    if (padding == 'PKCS7') {\n      return _processWithPaddedCipher(input, isEncrypt, mode, keyBytes, ivBytes, aesEngine);\n    }\n\n    // Raw block cipher / ZeroPadding path\n    return _processRawCipher(input, isEncrypt, mode, keyBytes, ivBytes, aesEngine);\n  }\n\n  // Build key bytes with required keySize length (pad/truncate handled where used)\n  static Uint8List _buildKeyBytes(String key, int keySize) {\n    final src = _decodeKeyStringToBytes(key);\n    final keyBytes = Uint8List(keySize);\n    for (int i = 0; i < keySize && i < src.length; i++) {\n      keyBytes[i] = src[i];\n    }\n    return keyBytes;\n  }\n\n  // Decode IV string to bytes (supports base64: prefix or plain text)\n  static Uint8List _buildIvBytes(String iv) {\n    return _decodeKeyStringToBytes(iv);\n  }\n\n  // Zero-padding helper for encryption\n  static Uint8List _padZeroForEncrypt(Uint8List input, int blockSize) {\n    final rem = input.length % blockSize;\n    if (rem == 0) return input;\n    final padLen = blockSize - rem;\n    final tmp = Uint8List(input.length + padLen);\n    tmp.setRange(0, input.length, input);\n    // trailing zeros already default to 0\n    return tmp;\n  }\n\n  static Uint8List _processWithPaddedCipher(Uint8List input, bool isEncrypt, String mode, Uint8List keyBytes,\n      Uint8List? ivBytes, AESEngine aesEngine) {\n    final BlockCipher blockCipher = (mode == 'CBC') ? CBCBlockCipher(aesEngine) : aesEngine;\n    final paddedCipher = PaddedBlockCipherImpl(PKCS7Padding(), blockCipher);\n\n    final params = (mode == 'CBC')\n        ? PaddedBlockCipherParameters<ParametersWithIV<KeyParameter>, Null>(\n            ParametersWithIV<KeyParameter>(KeyParameter(keyBytes), ivBytes!), null)\n        : PaddedBlockCipherParameters<KeyParameter, Null>(KeyParameter(keyBytes), null);\n\n    paddedCipher.init(isEncrypt, params);\n    return paddedCipher.process(input);\n  }\n\n  static Uint8List _processRawCipher(Uint8List input, bool isEncrypt, String mode, Uint8List keyBytes,\n      Uint8List? ivBytes, AESEngine aesEngine) {\n    final BlockCipher cipher = (mode == 'CBC') ? CBCBlockCipher(aesEngine) : aesEngine;\n\n    final CipherParameters params = (mode == 'CBC')\n        ? ParametersWithIV<KeyParameter>(KeyParameter(keyBytes), ivBytes!)\n        : KeyParameter(keyBytes);\n\n    cipher.init(isEncrypt, params);\n\n    if (input.length % cipher.blockSize != 0) {\n      throw ArgumentError('Input length must be multiple of block size (${cipher.blockSize}) for raw AES processing');\n    }\n\n    final out = Uint8List(input.length);\n    var offset = 0;\n    while (offset < input.length) {\n      final processed = cipher.process(input.sublist(offset, offset + cipher.blockSize));\n      out.setRange(offset, offset + processed.length, processed);\n      offset += cipher.blockSize;\n    }\n    return out;\n  }\n\n  // Decode key or iv string that may be prefixed with 'base64:' or be plain text\n  static Uint8List _decodeKeyStringToBytes(String s) {\n    if (s.startsWith('base64:')) {\n      final b64 = s.substring(7);\n      try {\n        return Uint8List.fromList(base64.decode(b64));\n      } catch (_) {\n        // fallback to utf8 bytes of the full string\n        return Uint8List.fromList(utf8.encode(s));\n      }\n    }\n\n    // default: treat as plain text\n    return Uint8List.fromList(utf8.encode(s));\n  }\n\n}"
  },
  {
    "path": "lib/utils/crypto_body_decoder.dart",
    "content": "import 'dart:convert';\nimport 'dart:typed_data';\n\nimport 'package:proxypin/network/http/http.dart';\n\nimport '../network/components/manager/request_crypto_manager.dart';\nimport '../network/util/logger.dart';\nimport 'aes.dart';\n\nclass CryptoDecodedResult {\n  final Uint8List bytes;\n  final String? text;\n  final CryptoRule? rule;\n\n  const CryptoDecodedResult({required this.bytes, this.text, this.rule});\n\n  bool get hasText => text != null && text!.trim().isNotEmpty;\n}\n\nclass CryptoBodyDecoder {\n  static Future<CryptoDecodedResult?> maybeDecode(HttpMessage message) async {\n    final ruleStore = await RequestCryptoManager.instance;\n\n    CryptoRule? match = ruleStore.getMatchingRule(message);\n    if (match == null) {\n      return null;\n    }\n\n    return _tryDecode(message, match.config, rule: match);\n  }\n\n  static CryptoDecodedResult? decode(HttpMessage message, CryptoKeyConfig config) {\n    return _tryDecode(message, config);\n  }\n\n  static CryptoDecodedResult? decodeWithConfig(HttpMessage message, CryptoKeyConfig config) {\n    return _tryDecode(message, config);\n  }\n\n  static CryptoDecodedResult? _tryDecode(HttpMessage message, CryptoKeyConfig config, {CryptoRule? rule}) {\n    final raw = message.body;\n    if (raw == null || raw.isEmpty || !config.isReady) {\n      return null;\n    }\n\n    // If rule specifies a field, try to parse body as JSON and extract that field for decryption\n    final fieldPath = rule?.field?.trim();\n    logger.d(\"CryptoBodyDecoder _tryDecode with config: $config and rule: $rule fieldPath: $fieldPath\");\n    if (fieldPath != null && fieldPath.isNotEmpty) {\n      // parse body as text\n      final content = _bytesToString(raw, message.charset);\n      if (content == null) return null;\n      dynamic jsonObj;\n      try {\n        jsonObj = jsonDecode(content);\n      } catch (_) {\n        return null;\n      }\n\n      final extracted = _extractJsonField(jsonObj, fieldPath);\n      if (extracted == null) return null;\n      // Only attempt when extracted is a string or number (we stringify otherwise)\n      String fieldStr = extracted.toString();\n\n      // build candidates from the field string: raw bytes and base64-decoded (if looks like base64)\n      final candidates = <Uint8List>[];\n      final base64Candidate = _tryDecodeBase64String(fieldStr);\n      if (base64Candidate != null) candidates.add(base64Candidate);\n\n      for (final candidate in candidates) {\n        try {\n          final decrypted = _decryptCandidate(candidate, config);\n          // print(\"CryptoBodyDecoder _tryDecode decrypted bytes: $decrypted\");\n          if (decrypted != null) {\n            return CryptoDecodedResult(bytes: decrypted, text: _bytesToString(decrypted, message.charset), rule: rule);\n          }\n        } catch (e) {\n          logger.d(\"CryptoBodyDecoder _tryDecode decryption error: $e\");\n          continue;\n        }\n      }\n      return null;\n    }\n\n    // whole-body: try raw bytes and base64-decoded text\n    final candidates = <Uint8List>[];\n    // candidates.add(Uint8List.fromList(raw));\n    final base64Candidate = _fromBase64(raw);\n    if (base64Candidate != null) {\n      candidates.add(base64Candidate);\n    }\n    // logger.d(\"CryptoBodyDecoder _tryDecode total candidates: ${candidates.length}\");\n    for (final candidate in candidates) {\n      try {\n        final decrypted = _decryptCandidate(candidate, config);\n        // logger.d(\"CryptoBodyDecoder _tryDecode decrypted bytes: $decrypted\");\n        if (decrypted != null) {\n          return CryptoDecodedResult(bytes: decrypted, text: _bytesToString(decrypted, message.charset), rule: rule);\n        }\n      } catch (e) {\n        logger.d(\"CryptoBodyDecoder _tryDecode decryption error: $e\");\n        continue;\n      }\n    }\n    return null;\n  }\n\n  // Attempt to decrypt a single candidate, handling ivSource == 'prefix' by extracting IV bytes.\n  static Uint8List? _decryptCandidate(Uint8List candidate, CryptoKeyConfig config) {\n    const int aesBlockSize = 16;\n    // If using prefix-mode, split IV and cipher bytes and ensure cipher bytes length is valid for non-PKCS7 paddings\n    if (config.mode == 'CBC' && config.ivSource == 'prefix') {\n      final n = config.ivPrefixLength;\n      if (candidate.length <= n) return null;\n      final ivBytes = candidate.sublist(0, n);\n      final cipherBytes = candidate.sublist(n);\n      // For non-PKCS7 paddings (e.g., ZeroPadding/raw) the cipher bytes length must be multiple of block size\n      if (config.padding != 'PKCS7' && (cipherBytes.length % aesBlockSize != 0)) return null;\n      final ivStr = 'base64:' + base64.encode(ivBytes);\n      try {\n        return AesUtils.decrypt(cipherBytes,\n            key: config.key, keyLength: config.keyLength, mode: config.mode, padding: config.padding, iv: ivStr);\n      } catch (e) {\n        logger.d('CryptoBodyDecoder _decryptCandidate error (prefix): $e');\n        return null;\n      }\n    } else {\n      // iv provided in config.iv (may include base64: prefix or be plain text)\n      // For non-PKCS7 paddings ensure candidate length is block-aligned before attempting raw decrypt\n      if (config.padding != 'PKCS7' && (candidate.length % aesBlockSize != 0)) return null;\n      final ivParam = (config.mode == 'CBC') ? config.iv : null;\n      try {\n        return AesUtils.decrypt(candidate,\n            key: config.key, keyLength: config.keyLength, mode: config.mode, padding: config.padding, iv: ivParam);\n      } catch (e) {\n        logger.d('CryptoBodyDecoder _decryptCandidate error: $e');\n        return null;\n      }\n    }\n  }\n\n  // Try to decode a base64 string; return bytes or null\n  static Uint8List? _tryDecodeBase64String(String s) {\n    final trimmed = s.trim();\n    if (trimmed.isEmpty) return null;\n    if (!_maybeBase64(trimmed)) return null;\n    try {\n      return Uint8List.fromList(base64.decode(trimmed));\n    } catch (_) {\n      return null;\n    }\n  }\n\n  // Extract a nested JSON field by a dot-separated path. Supports array indexes like items[0].value\n  static dynamic _extractJsonField(dynamic jsonObj, String path) {\n    final parts = path.split('.');\n    dynamic current = jsonObj;\n    for (final part in parts) {\n      if (current == null) return null;\n      // check for array index like key[index]\n      final arrayMatch = RegExp(r\"^([a-zA-Z0-9_\\-]+)\\[(\\d+)\\]\").firstMatch(part);\n      if (arrayMatch != null) {\n        final key = arrayMatch.group(1)!;\n        final idx = int.parse(arrayMatch.group(2)!);\n        if (current is Map && current.containsKey(key)) {\n          final list = current[key];\n          if (list is List && idx >= 0 && idx < list.length) {\n            current = list[idx];\n            continue;\n          }\n          return null;\n        }\n        return null;\n      }\n\n      // normal key or numeric index for lists\n      if (current is Map) {\n        if (!current.containsKey(part)) return null;\n        current = current[part];\n      } else if (current is List) {\n        final idx = int.tryParse(part);\n        if (idx == null || idx < 0 || idx >= current.length) return null;\n        current = current[idx];\n      } else {\n        return null;\n      }\n    }\n    return current;\n  }\n\n  static Uint8List? _fromBase64(List<int> raw) {\n    try {\n      final content = utf8.decode(raw).trim();\n      if (content.isEmpty || !_maybeBase64(content)) {\n        return null;\n      }\n      return Uint8List.fromList(base64.decode(content));\n    } catch (_) {\n      return null;\n    }\n  }\n\n  static bool _maybeBase64(String value) {\n    if (value.length % 4 != 0) return false;\n    if (value.contains(RegExp(r'[^A-Za-z0-9+/=\\r\\n]'))) return false;\n    return true;\n  }\n\n  static String? _bytesToString(List<int> bytes, String? charset) {\n    try {\n      if (charset == null || charset.toLowerCase().contains('utf')) {\n        return utf8.decode(bytes);\n      }\n      return const Latin1Codec().decode(bytes);\n    } catch (_) {\n      try {\n        return utf8.decode(bytes);\n      } catch (_) {\n        return null;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/curl.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\nimport 'package:proxypin/utils/lang.dart';\n\n///复制cURL请求\nString curlRequest(HttpRequest request) {\n  List<String> headers = [];\n  request.headers.forEach((key, values) {\n    for (var val in values) {\n      headers.add(\"  -H '$key: $val' \");\n    }\n  });\n\n  String body = '';\n  if (request.bodyAsString.isNotEmpty) {\n    body = \"  --data '${request.bodyAsString}' \\\\\\n\";\n  }\n  return \"curl -X ${request.method.name} '${request.requestUrl}' \\\\\\n\"\n      \"${headers.join('\\\\\\n')} \\\\\\n $body  --compressed\";\n}\n\nmain() {\n  print(Curl.parse(\n      \"curl -X POST 'https://example.com/api' -H 'Content-Type: application/json' -d '{\\\"key\\\":\\\"value\\\"}'\"));\n}\n\nclass Curl {\n  static const String _h = \"-H\";\n  static const String _header = \"--header\";\n  static const String _x = \"-X\";\n  static const String _request = \"--request\";\n  static const String _data = \"--data\";\n  static const String _dataRaw = \"--data-raw\";\n  static const String _d = \"-d\";\n\n  static HttpRequest parse(String curlCommand) {\n    HttpMethod method = HttpMethod.get;\n    HttpHeaders headers = HttpHeaders();\n\n    String? url;\n    String? data;\n\n    // 去除 \"curl\" 关键字并去除首尾空格\n    String trimmedCommand = curlCommand.replaceFirst('curl', '').trim();\n\n    List<String> parts = [];\n    String currentPart = '';\n    bool inQuotes = false;\n    bool inBody = false;\n\n    // 处理可能包含引号的参数\n    for (int i = 0; i < trimmedCommand.length; i++) {\n      String char = trimmedCommand[i];\n      if (char == '\"' || char == \"'\") {\n        if (inBody) {\n          currentPart += char;\n          continue;\n        }\n\n        // 如果当前字符是引号，切换 inQuotes 状态\n        inQuotes = !inQuotes;\n      } else if (char == ' ' && !inQuotes) {\n        if (inBody && currentPart.length > 2) {\n          // 如果当前部分是数据，去掉前后的引号\n          currentPart = currentPart.substring(1, currentPart.length - 1);\n        }\n\n        if (currentPart == '-d' || currentPart == '--data' || currentPart == '--data-raw') {\n          inBody = true;\n        } else {\n          inBody = false;\n        }\n\n        parts.add(currentPart);\n        currentPart = '';\n      } else {\n        currentPart += char;\n      }\n    }\n\n    if (currentPart.isNotEmpty) {\n      if (inBody && currentPart.length > 2) {\n        // 如果当前部分是数据，去掉前后的引号\n        currentPart = currentPart.substring(1, currentPart.length - 1);\n      }\n\n      parts.add(currentPart);\n    }\n\n    String protocolVersion = \"HTTP/1.1\";\n\n    // 遍历参数列表进行解析\n    for (int i = 0; i < parts.length; i++) {\n      String part = parts[i];\n      if (part == _x || part == _request) {\n        // 解析请求方法\n        if (i + 1 < parts.length) {\n          method = HttpMethod.valueOf(parts[++i]);\n        }\n      } else if (part == _h || part == _header) {\n        // 解析请求头\n        if (i + 1 < parts.length) {\n          String headerStr = parts[++i];\n          List<String> headerParts = headerStr.splitFirst(':'.codeUnits.first);\n          if (headerParts.length == 2) {\n            headers.add(headerParts[0], headerParts[1]);\n          }\n        }\n      } else if (part == _d || part == _dataRaw || part == _data) {\n        // 解析请求数据\n        if (i + 1 < parts.length) {\n          data = parts[++i];\n        }\n      } else if (url == null && !part.startsWith('-') && part.contains(\"http\")) {\n        // 解析请求 URL\n        url = part.replaceAll(\"'\", \"\").replaceAll('\"', '');\n      } else if (\"--http2\" == part) {\n        // protocolVersion = \"HTTP2\";\n      }\n    }\n\n    if (data?.isNotEmpty == true && method == HttpMethod.get) {\n      method = HttpMethod.post;\n    }\n\n    HttpRequest request = HttpRequest(method, url ?? '', protocolVersion: protocolVersion);\n    request.headers.addAll(headers);\n    request.body = data?.codeUnits;\n    return request;\n  }\n}\n\n//判断是否结束\nint endIndex(String str) {\n  for (int i = 0; i < str.length; i++) {\n    if (str[i] == '\\'') {\n      if (i == 0 || str[i - 1] != '\\\\') {\n        return i;\n      }\n    }\n  }\n  return -1;\n}\n"
  },
  {
    "path": "lib/utils/desktop_support.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:window_manager/window_manager.dart';\n\nimport '../network/util/logger.dart';\nimport '../ui/component/multi_window.dart';\n\nclass DesktopSupport {\n  static Future<void> initialize(AppConfiguration appConfiguration) async {\n    try {\n      await windowManager.ensureInitialized();\n\n      //设置窗口大小\n      Size windowSize =\n          appConfiguration.windowSize ?? (Platform.isMacOS ? const Size(1230, 750) : const Size(1100, 650));\n      WindowOptions windowOptions =\n          WindowOptions(minimumSize: const Size(1000, 600), size: windowSize, titleBarStyle: TitleBarStyle.hidden);\n\n      Offset? windowPosition = appConfiguration.windowPosition;\n\n      if (appConfiguration.themeMode != ThemeMode.system) {\n        windowManager.setBrightness(appConfiguration.themeMode == ThemeMode.dark ? Brightness.dark : Brightness.light);\n      }\n\n      if (Platform.isMacOS) {\n        // try {\n        //   await WindowManipulator.initialize();\n        //   // 调整关闭按钮的位置\n        //   WindowManipulator.overrideStandardWindowButtonPosition(\n        //       buttonType: NSWindowButtonType.closeButton, offset: Offset(10, 13));\n        //   WindowManipulator.overrideStandardWindowButtonPosition(\n        //       buttonType: NSWindowButtonType.miniaturizeButton, offset: const Offset(32, 13));\n        //   WindowManipulator.overrideStandardWindowButtonPosition(\n        //       buttonType: NSWindowButtonType.zoomButton, offset: const Offset(52, 13));\n        // } catch (e) {\n        //   logger.e(\"Error adjusting macOS window button positions: $e\");\n        // }\n      }\n\n      await windowManager.waitUntilReadyToShow(windowOptions, () async {\n        if (windowPosition != null) {\n          await windowManager.setPosition(windowPosition);\n        }\n\n        await windowManager.show();\n        await windowManager.focus();\n      });\n\n      registerMethodHandler();\n    } catch (e) {\n      logger.e(\"Error during desktop initialization: $e\");\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/export_request.dart",
    "content": "import 'dart:convert';\nimport 'dart:typed_data';\n\nimport 'package:file_picker/file_picker.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/util/logger.dart';\nimport 'package:proxypin/ui/component/utils.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:proxypin/utils/har.dart';\n\nvoid exportRequest(HttpRequest request) async {\n  String fileName = \"request_${request.hostAndPort?.host}_${request.requestId}.txt\";\n  var json = copyRawRequest(request);\n\n  var path = await FilePicker.platform.saveFile(fileName: fileName, bytes: utf8.encode(json));\n  logger.d(\"Export request to $path\");\n}\n\nvoid exportRequestBody(HttpRequest request) async {\n  String fileName = \"request_body_${request.hostAndPort?.host}_${request.requestId}.txt\";\n\n  var path = await FilePicker.platform\n      .saveFile(fileName: fileName, bytes: request.body == null ? null : Uint8List.fromList(request.body!));\n  logger.d(\"Export request body to $path\");\n}\n\nvoid exportResponse(HttpResponse? response) async {\n  if (response == null) {\n    return;\n  }\n\n  String fileName = \"response_${response.request?.hostAndPort?.host}_${response.requestId}.txt\";\n\n  Future<String> copyRawResponse(HttpResponse response) async {\n    var sb = StringBuffer();\n    sb.writeln(\"${response.protocolVersion} ${response.status.code} ${response.status.reasonPhrase}\");\n    sb.write(response.headers.headerLines());\n    if (response.bodyAsString.isNotEmpty) {\n      sb.writeln();\n      sb.write(await response.decodeBodyString());\n    }\n    return sb.toString();\n  }\n\n  var json = await copyRawResponse(response);\n  var path = await FilePicker.platform.saveFile(fileName: fileName, bytes: utf8.encode(json));\n  logger.d(\"Export response to $path\");\n}\n\nvoid exportResponseBody(HttpResponse? response) async {\n  if (response == null) {\n    return;\n  }\n\n  String fileName = \"response_body_${response.request?.hostAndPort?.host}_${response.requestId}.txt\";\n\n  var path = await FilePicker.platform\n      .saveFile(fileName: fileName, bytes: response.body == null ? null : Uint8List.fromList(response.body!));\n  logger.d(\"Export response body to $path\");\n}\n\nvoid exportHar(HttpRequest request) async {\n  String fileName = \"har_${request.hostAndPort?.host}_${request.requestId}.har\";\n\n  var entry = Har.toHar(request);\n  print(entry);\n  var har = {\n    \"log\": {\n      \"version\": \"1.2\",\n      \"creator\": {\"name\": \"ProxyPin\", \"version\": AppConfiguration.version},\n      \"pages\": [\n        {\n          \"title\": \"ProxyPin Har Export\",\n          \"id\": \"ProxyPin\",\n          \"startedDateTime\": request.requestTime.toUtc().toIso8601String(),\n          \"pageTimings\": {\"onContentLoad\": -1, \"onLoad\": -1}\n        }\n      ],\n      \"entries\": [entry],\n    }\n  };\n  var json = jsonEncode(har);\n\n  var path = await FilePicker.platform.saveFile(fileName: fileName, bytes: utf8.encode(json));\n  logger.d(\"Export har to $path\");\n}\n"
  },
  {
    "path": "lib/utils/files.dart",
    "content": "\n\nimport 'dart:io';\n\nclass Files {\n  //获取文件名称\n\n  static String getName(String path) {\n    var index = path.lastIndexOf(Platform.pathSeparator);\n    return path.substring(index + 1);\n  }\n}\n"
  },
  {
    "path": "lib/utils/font.dart",
    "content": "import 'dart:io';\n\nclass Fonts {\n  String thin = \"PingFangSC-Thin\";\n  String light = \"PingFangSC-Light\";\n  String regular = \"PingFangSC-Regular\";\n  String medium = \"PingFangSC-Medium\";\n  String semibold = \"PingFangSC-Semibold\";\n  String bold = \"PingFangSC-Bold\";\n}\n\nclass AppleFonts extends Fonts {\n  @override\n  String thin = \"PingFangSC-Thin\";\n  @override\n  String light = \"PingFangSC-Light\";\n  @override\n  String regular = \"PingFang SC\";\n\n  @override\n  var medium = \"PingFangSC-Medium\";\n  @override\n  var semibold = \"PingFangSC-Semibold\";\n  @override\n  String bold = \"PingFangSC-Bold\";\n}\n\nclass WindowsFonts extends Fonts {\n  String thin = \"Microsoft YaHei UI Light\";\n  String light = \"Microsoft YaHei UI Light\";\n  String regular = \"Microsoft YaHei UI\";\n  String medium = \"Microsoft YaHei UI\";\n  String semibold = \"Microsoft YaHei UI Bold\";\n  String bold = \"Microsoft YaHei UI Bold\";\n}\n\nclass AndroidFonts extends Fonts {\n  String thin = \"sans-serif-thin\";\n  String light = \"sans-serif-light\";\n  String regular = \"sans-serif\";\n  String medium = \"sans-serif-medium\";\n  String semibold =\n      \"sans-serif-medium\"; // Android doesn't have a specific semibold, using medium.\n  String bold = \"sans-serif-bold\";\n}\n\nFonts fonts = Platform.isAndroid\n    ? AndroidFonts()\n    : Platform.isWindows\n        ? WindowsFonts()\n        : AppleFonts();\n"
  },
  {
    "path": "lib/utils/har.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:proxypin/network/channel/host_port.dart';\nimport 'package:proxypin/network/http/content_type.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/http_headers.dart';\nimport 'package:proxypin/network/util/process_info.dart';\nimport 'package:proxypin/ui/configuration.dart';\n\nclass Har {\n  static int maxBodyLength = 1024 * 1024 * 4;\n\n  static List<Map> _entries(List<HttpRequest> list) {\n    return list.map((e) => toHar(e)).toList();\n  }\n\n  static Map toHar(HttpRequest request) {\n    Map har = {\n      \"startedDateTime\": request.requestTime.toUtc().toIso8601String(), // 请求发出的时间(ISO 8601)\n      \"time\": request.response?.responseTime.difference(request.requestTime).inMilliseconds ?? -1, // 请求耗时，单位毫秒\n      \"pageref\": \"ProxyPin\", // 页面标识\n      \"_id\": request.requestId, // 页面标识\n      '_app': request.processInfo?.toJson(),\n      \"request\": {\n        \"method\": request.method.name, // 请求方法\n        \"url\": request.requestUrl, // 请求地址\n        \"httpVersion\": request.protocolVersion, // HTTP协议版本\n        \"cookies\": [], // 请求携带的cookie\n        \"headers\": _headers(request), // 请求头\n        \"queryString\": _getQueryString(request), // 请求参数\n        \"postData\": _getPostData(request), // 请求体\n        \"headersSize\": -1, // 请求头大小\n        \"bodySize\": request.body?.length ?? -1, // 请求体大小\n      },\n      \"cache\": {},\n      'timings': {\n        'send': 0,\n        'wait': request.response?.responseTime.difference(request.requestTime).inMilliseconds ?? -1,\n        'receive': 0,\n      },\n      'serverIPAddress': request.response?.remoteHost ?? '', // 服务器IP地址\n    };\n\n    har['response'] = {\n      \"status\": request.response?.status.code ?? 0, // 响应状态码\n      \"statusText\": request.response?.status.reasonPhrase ?? '', // 响应状态码描述\n      \"httpVersion\": request.response?.protocolVersion ?? 'HTTP/1.1', // HTTP协议版本\n      \"cookies\": [], // 响应携带的cookie\n      \"headers\": _headers(request.response), // 响应头\n      \"content\": {\n        \"size\": request.response?.body?.length ?? -1, // 响应体大小\n        \"mimeType\": _getContentType(request.response?.headers.contentType), // 响应体类型\n        \"text\": request.response?.bodyAsString ?? '', // 响应体内容\n      },\n      \"redirectURL\": '', // 重定向地址\n      \"headersSize\": -1, // 响应头大小\n      \"bodySize\": request.response?.body?.length ?? -1, // 响应体大小\n    };\n    return har;\n  }\n\n  static Future<String> writeJson(List<HttpRequest> list, {String title = ''}) async {\n    var entries = _entries(list);\n    Map har = {};\n    title = title.contains(\"ProxyPin\") ? title : \"[ProxyPin]$title\";\n    har[\"log\"] = {\n      \"version\": \"1.2\",\n      \"creator\": {\"name\": \"ProxyPin\", \"version\": AppConfiguration.version},\n      \"pages\": [\n        {\n          \"title\": title,\n          \"id\": \"ProxyPin\",\n          \"startedDateTime\": list.firstOrNull?.requestTime.toUtc().toIso8601String(),\n          \"pageTimings\": {\"onContentLoad\": -1, \"onLoad\": -1}\n        }\n      ],\n      \"entries\": entries,\n    };\n    return jsonEncode(har);\n  }\n\n  static Future<File> writeFile(List<HttpRequest> list, File file, {String title = ''}) async {\n    var json = await writeJson(list, title: title);\n    return file.writeAsString(json);\n  }\n\n  //读取文件\n  static Future<List<HttpRequest>> readFile(File file) async {\n    var lines = await file.readAsLines();\n    List<HttpRequest> list = [];\n\n    for (var value in lines) {\n      var har = jsonDecode(value.substring(0, value.length - 1));\n      var request = toRequest(har);\n      list.add(request);\n    }\n    return list;\n  }\n\n  static List<Map> _headers(HttpMessage? message) {\n    var headers = <Map<String, String>>[];\n    var contentEncodingName = message?.headers.getOriginalName(HttpHeaders.CONTENT_ENCODING);\n\n    message?.headers.forEach((name, values) {\n      for (var element in values) {\n        //body已经解码 删除编码\n        if (name == contentEncodingName && element == 'br') {\n          continue;\n        }\n        headers.add({'name': name, 'value': element});\n      }\n    });\n    return headers;\n  }\n\n  /// har to request\n  static HttpRequest toRequest(Map har) {\n    var request = har['request'];\n    var method = request['method'];\n    List headers = request['headers'];\n\n    var httpRequest = HttpRequest(HttpMethod.valueOf(method), request['url'], protocolVersion: request['httpVersion']);\n    if (har.containsKey(\"_id\")) httpRequest.requestId = har['_id'].toString(); // 页面标识\n    httpRequest.processInfo = har['_app'] == null ? null : ProcessInfo.fromJson(har['_app']);\n    httpRequest.body = request['postData']?['text']?.toString().codeUnits;\n    for (var element in headers) {\n      httpRequest.headers.add(element['name'], element['value']);\n    }\n    var response = har['response'];\n    HttpResponse? httpResponse;\n    if (response != null && response['status'] != null) {\n      httpResponse = HttpResponse(HttpStatus.newStatus(response['status'], response['statusText']),\n          protocolVersion: response['httpVersion']);\n      httpResponse.body = response['content']['text']?.toString().codeUnits;\n      List responseHeaders = response['headers'];\n      for (var element in responseHeaders) {\n        httpResponse.headers.add(element['name'], element['value']);\n      }\n    }\n\n    httpRequest.response = httpResponse;\n    httpResponse?.request = httpRequest;\n    httpRequest.hostAndPort = HostAndPort.of(httpRequest.requestUrl);\n    httpResponse?.remoteHost = har['serverIPAddress']; // 服务器IP地址\n\n    //请求时间\n    if (har['startedDateTime'] != null) {\n      httpRequest.requestTime = DateTime.parse(har['startedDateTime']).toLocal();\n    }\n    if (har['time'] != null) {\n      httpRequest.response?.responseTime =\n          httpRequest.requestTime.add(Duration(milliseconds: double.parse(har['time'].toString()).toInt()));\n    }\n    return httpRequest;\n  }\n\n  static List<Map<String, String>> _getQueryString(HttpRequest request) {\n    final queryStringList = <Map<String, String>>[];\n    final queries = request.requestUri?.queryParametersAll;\n    queries?.forEach((key, valueList) {\n      for (var value in valueList) {\n        queryStringList.add({\"name\": key, \"value\": value});\n      }\n    });\n    return queryStringList;\n  }\n\n  static Map<String, dynamic> _getPostData(HttpRequest request) {\n    if (request.contentType == ContentType.formData || request.contentType == ContentType.formUrl) {\n      return {\n        \"mimeType\": request.headers.contentType, // 请求体类型\n        if (request.body != null) \"text\": String.fromCharCodes(request.body!), // 请求体内容\n        \"params\": [], // 请求体内容\n      };\n    }\n    return {\n      \"mimeType\": request.headers.contentType, // 请求体类型\n      if (request.body != null) \"text\": String.fromCharCodes(request.body!), // 请求体内容\n    };\n  }\n\n  //获取contentType\n  static String? _getContentType(String? type) {\n    if (type == null) {\n      return '';\n    }\n    var indexOf = type.indexOf(\"charset=\");\n    if (indexOf == -1) {\n      return type;\n    }\n    var contentType = type.substring(0, indexOf).trimRight();\n    if (contentType.endsWith(\";\")) {\n      return contentType.substring(0, contentType.length - 1);\n    }\n    return contentType;\n  }\n}\n"
  },
  {
    "path": "lib/utils/ip.dart",
    "content": "import 'dart:io';\n\nvoid main() {\n  NetworkInterface.list(type: InternetAddressType.IPv4).then((interfaces) {\n    for (var interface in interfaces) {\n      print(interface.name);\n      for (var address in interface.addresses) {\n        print(\"  ${address.address}\");\n        print(\"  ${address.host}\");\n        print(\"  ${address.type}\");\n      }\n    }\n  });\n}\n\nString? ip;\n\n/// 获取本机ip (en0 or WLAN)优先\nFuture<String> localIp({bool readCache = true}) async {\n  if (!readCache) {\n    ip = null;\n  }\n  ip ??= await localAddress().then((value) => value.address);\n  return ip!;\n}\n\nFuture<InternetAddress> localAddress() async {\n  return await NetworkInterface.list(type: InternetAddressType.IPv4).then((interfaces) {\n    interfaces.sort((a, b) {\n      return weight(a) - weight(b);\n    });\n    return interfaces.first.addresses.first;\n  });\n}\n\nList<String>? ipList;\n\n/// 获取本机所有ip\nFuture<List<String>> localIps({bool readCache = true}) async {\n  if (readCache && ipList != null) {\n    return ipList!;\n  }\n\n  var list = await NetworkInterface.list(type: InternetAddressType.IPv4);\n  list.sort((a, b) {\n    return weight(a) - weight(b);\n  });\n\n  ipList = [];\n  for (var element in list) {\n    if (!ipList!.contains(element.addresses.first.address)) {\n      ipList?.add(element.addresses.first.address);\n    }\n  }\n  return ipList!;\n}\n\nFuture<String> networkName() {\n  return NetworkInterface.list(type: InternetAddressType.IPv4).then((interfaces) {\n    interfaces.sort((a, b) {\n      return weight(a) - weight(b);\n    });\n    return interfaces.first.name;\n  });\n}\n\n// en0(macos系统) or WLAN(widows设备名)优先\nint weight(NetworkInterface it) {\n  if (it.name.toUpperCase().startsWith('WLAN')) {\n    return -10;\n  }\n  if (it.name == 'en0') {\n    return -1;\n  }\n  if (it.name.startsWith('ccmn')) {\n    return 0;\n  }\n  return 1;\n}\n"
  },
  {
    "path": "lib/utils/keyword_highlight.dart",
    "content": "import 'dart:convert';\n\nimport 'package:flutter/material.dart';\nimport 'package:proxypin/ui/configuration.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nclass KeywordHighlights {\n  static bool _enabled = true;\n  static bool initialized = false;\n  static const String storeKey = \"highlightKeywords\";\n\n  static final ValueNotifier _keywordsController = ValueNotifier<Map<Color, String>>({});\n\n  static Map<Color, String> get keywords => _keywordsController.value;\n\n  static bool get enabled => _enabled;\n\n  static set enabled(bool value) {\n    _enabled = value;\n    SharedPreferences.getInstance().then((prefs) {\n      prefs.setBool('highlightEnabled', value);\n    });\n  }\n\n  static Color? getHighlightColor(String? key) {\n    if (key == null || !_enabled) {\n      return null;\n    }\n    for (var entry in _keywordsController.value.entries) {\n      if (key.contains(entry.value)) {\n        return entry.key;\n      }\n    }\n    return null;\n  }\n\n  static addListener(VoidCallback listener) {\n    if (!initialized) {\n      initialized = true;\n      SharedPreferences.getInstance().then((prefs) {\n        var enabledVal = prefs.getBool('highlightEnabled');\n        if (enabledVal != null) {\n          enabled = enabledVal;\n        }\n\n        var val = prefs.getString(storeKey);\n        if (val == null) {\n          return;\n        }\n        var map = jsonDecode(val);\n        map.forEach((key, value) {\n          var color = ColorMapping.getColor(key);\n          _keywordsController.value[color] = value;\n        });\n      });\n    }\n    _keywordsController.addListener(listener);\n  }\n\n  static Future<void> saveKeywords(Map<Color, String> keywords) async {\n    SharedPreferences prefs = await SharedPreferences.getInstance();\n    var map = keywords.map((key, value) => MapEntry(ColorMapping.getColorName(key), value));\n    prefs.setString(storeKey, jsonEncode(map));\n    _keywordsController.value = keywords;\n  }\n\n  static removeListener(VoidCallback listener) {\n    _keywordsController.removeListener(listener);\n  }\n}\n"
  },
  {
    "path": "lib/utils/lang.dart",
    "content": "import 'dart:convert';\n\nimport 'package:date_format/date_format.dart';\nimport 'package:flutter/material.dart';\n\n/// @author wanghongen\n/// 2023/10/8\nextension ListFirstWhere<T> on Iterable<T> {\n  T? firstWhereOrNull(bool Function(T) test) {\n    try {\n      return firstWhere(test);\n    } on StateError {\n      return null;\n    }\n  }\n\n  T elementAtOrElse(int index, T Function(int index) defaultValue) {\n    if (index < 0) return defaultValue(index);\n    var count = 0;\n    for (final element in this) {\n      if (index == count++) return element;\n    }\n    return defaultValue(index);\n  }\n}\n\nextension DateTimeFormat on DateTime {\n  String format() {\n    return formatDate(this, [yyyy, '-', mm, '-', dd, ' ', HH, ':', nn, ':', ss]);\n  }\n\n  String formatMillisecond() {\n    return formatDate(this, [yyyy, '-', mm, '-', dd, ' ', HH, ':', nn, ':', ss, '.', SSS]);\n  }\n\n  String dateFormat() {\n    return formatDate(this, [yyyy, '-', mm, '-', dd]);\n  }\n\n  String timeFormat() {\n    return formatDate(this, [HH, ':', nn, ':', ss]);\n  }\n}\n\nclass JSON {\n  ///  格式化json\n  static String pretty(String jsonString) {\n    try {\n      var jsonObject = jsonDecode(jsonString);\n      var encoder = JsonEncoder.withIndent('  ');\n      return encoder.convert(jsonObject);\n    } catch (e) {\n      return jsonString;\n    }\n  }\n\n  /// 压缩json\n  static String compact(String jsonString) {\n    try {\n      var jsonObject = jsonDecode(jsonString);\n      return jsonEncode(jsonObject);\n    } catch (e) {\n      return jsonString;\n    }\n  }\n}\n\nclass ValueWrap<V> {\n  V? _v;\n\n  ValueWrap();\n\n  factory ValueWrap.of(V v) {\n    var valueWrap = ValueWrap<V>();\n    valueWrap._v = v;\n    return valueWrap;\n  }\n\n  void set(V? v) => this._v = v;\n\n  V? get() => this._v;\n\n  bool isNull() => this._v == null;\n}\n\nclass Strings {\n  static MapEntry<String, String>? splitFirst(String str, Pattern pattern) {\n    var index = str.indexOf(pattern);\n    if (index > 0) {\n      return MapEntry(str.substring(0, index), str.substring(index + 1));\n    }\n\n    return null;\n  }\n\n  static String trimWrap(String str, String wrap) {\n    if (str.startsWith(wrap) && str.endsWith(wrap)) {\n      return str.substring(1, str.length - 1);\n    }\n    return str;\n  }\n\n  ///防止文字自动换行\n  static String autoLineString(String str) {\n    return str.fixAutoLines();\n  }\n}\n\n/// 防止文字自动换行\n/// 当中英文混合，或者中文与数字或者特殊符号，或则英文单词时，文本会被自动换行，\n/// 这样会导致，换行时上一行可能会留很大的空白区域\n/// 把每个字符插入一个0宽的字符， \\u{200B}\nextension StringEnhance on String {\n\n  String removePrefix(String prefix) {\n    if (startsWith(prefix)) {\n      return substring(prefix.length, length);\n    } else {\n      return this;\n    }\n  }\n\n  String fixAutoLines() {\n    return Characters(this).join('\\u{200B}');\n  }\n\n  List<String> splitFirst(int code) {\n    var index = codeUnits.indexOf(code);\n    if (index == -1) {\n      return [this];\n    }\n    var key = substring(0, index).trim();\n    var value = substring(index + 1).trim();\n    return [key, value];\n  }\n\n  String camelCaseToSpaced() {\n    var input = this;\n    return input.replaceAllMapped(RegExp(r'([a-z])([A-Z])'), (Match match) {\n      return '${match.group(1)} ${match.group(2)}';\n    }).toLowerCase();\n  }\n}\n\nclass Pair<K, V> {\n  final K? key;\n  V? value;\n\n  Pair(this.key, this.value);\n}\n\nclass Maps {\n  static K? getKey<K, V>(Map<K, V> map, V? value) {\n    for (var entry in map.entries) {\n      if (entry.value == value) {\n        return entry.key;\n      }\n    }\n    return null;\n  }\n}\n\n/// 用于存储一些数据，当数据超过指定大小时，删除最早的数据\nclass CapacityList<T> {\n  final int capacity;\n  final List<T> list = [];\n\n  CapacityList(this.capacity);\n\n  void add(T value) {\n    if (list.length >= capacity) {\n      list.removeAt(0);\n    }\n    list.add(value);\n  }\n\n  void remove(T value) {\n    list.remove(value);\n  }\n\n  void clear() {\n    list.clear();\n  }\n}\n"
  },
  {
    "path": "lib/utils/listenable_list.dart",
    "content": "/*\n * Copyright 2023 Hongen Wang\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 */\nabstract class ListenerListEvent<T> {\n  /// 监听的源\n  sourceAware(List<T> source) {}\n\n  void onAdd(T item);\n\n  void onRemove(T item);\n\n  void onUpdate(T item) {}\n\n  void onBatchRemove(List<T> items);\n\n  clear();\n}\n\nclass OnchangeListEvent<T> extends ListenerListEvent<T> {\n  final Function onChange;\n\n  OnchangeListEvent(this.onChange);\n\n  @override\n  void onAdd(T item) => onChange.call();\n\n  @override\n  void onRemove(T item) => onChange.call();\n\n  @override\n  void onUpdate(T item) => onChange.call();\n\n  @override\n  void onBatchRemove(List<T> items) => onChange.call();\n\n  @override\n  clear() => onChange.call();\n}\n\n/// 可监听list\n/// @author wanghongen\n/// 2024/01/30\nclass ListenableList<T> extends Iterable<T> {\n  List<T> source = [];\n  final List<ListenerListEvent<T>> _listeners = [];\n\n  ListenableList([List<T>? source]) {\n    if (source != null) this.source = source;\n  }\n\n  addListener(ListenerListEvent<T> listener) {\n    if (_listeners.contains(listener)) return;\n    listener.sourceAware(source);\n    _listeners.add(listener);\n  }\n\n  removeListener(ListenerListEvent<T> listener) {\n    _listeners.remove(listener);\n  }\n\n  @override\n  int get length => source.length;\n\n  @override\n  bool get isEmpty => source.isEmpty;\n\n  int indexOf(T item) => source.indexOf(item);\n\n  @override\n  T elementAt(int index) => source[index];\n\n  List<T> sublist(int start, [int? end]) {\n    return source.sublist(start, end);\n  }\n\n  void removeRange(start, end) {\n    source.removeRange(start, end > source.length ? source.length : end);\n    for (var element in _listeners) {\n      element.clear();\n    }\n  }\n\n  update(int index, T item) {\n    source[index] = item;\n    for (var element in _listeners) {\n      element.onUpdate(item);\n    }\n  }\n\n  add(T item) {\n    source.add(item);\n    for (var element in _listeners) {\n      element.onAdd(item);\n    }\n  }\n\n  bool remove(T item) {\n    var remove = source.remove(item);\n    if (remove) {\n      for (var element in _listeners) {\n        element.onRemove(item);\n      }\n    }\n    return remove;\n  }\n\n  T removeAt(int index) {\n    var item = source.removeAt(index);\n    if (item != null) {\n      for (var element in _listeners) {\n        element.onRemove(item);\n      }\n    }\n    return item;\n  }\n\n  clear() {\n    source.clear();\n    for (var element in _listeners) {\n      element.clear();\n    }\n  }\n\n  removeWhere(bool Function(T element) test) {\n    var list = <T>[];\n    source.removeWhere((it) {\n      if (test.call(it)) {\n        list.add(it);\n        return true;\n      }\n      return false;\n    });\n    if (list.isEmpty) return;\n\n    for (var element in _listeners) {\n      element.onBatchRemove(list);\n    }\n  }\n\n  @override\n  Iterator<T> get iterator => source.iterator;\n}\n"
  },
  {
    "path": "lib/utils/navigator.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass NavigatorHelper {\n  static final NavigatorHelper _instance = NavigatorHelper._internal();\n\n  //私有构造方法\n  NavigatorHelper._internal();\n\n  factory NavigatorHelper() {\n    return _instance;\n  }\n\n  GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;\n\n  BuildContext get context => NavigatorHelper().navigatorKey.currentState!.context;\n\n  //保存单例\n  final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();\n\n  //返回上一页\n  static void pop<T extends Object?>([T? result]) {\n    Navigator.of(NavigatorHelper().context).pop<T>(result);\n  }\n\n  //跳转到指定页面\n  static Future<T?> push<T extends Object?>(Route<T> route) {\n    return Navigator.of(NavigatorHelper().context).push(route);\n  }\n\n  //返回上一页\n  static Future<bool> maybePop<T extends Object?>([T? result]) {\n    return Navigator.of(NavigatorHelper().context).maybePop<T>(result);\n  }\n}\n\n///定义全局的NavigatorHelper对象，页面引入该文件后可以直接使用\nNavigatorHelper navigatorHelper = NavigatorHelper();\n\nclass NavigatorPage extends StatelessWidget {\n  final GlobalKey navigatorKey;\n  final Widget child;\n\n  const NavigatorPage({super.key, required this.child, required this.navigatorKey});\n\n  bool onPopInvoked() {\n    var context = navigatorKey.currentState?.context;\n    if (context == null) return false;\n    if (Navigator.canPop(context)) {\n      Navigator.maybePop(context);\n      return true;\n    }\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n        child: Navigator(\n      key: navigatorKey,\n      initialRoute: '/',\n      onGenerateRoute: (settings) {\n        return MaterialPageRoute(builder: (context) => child, settings: settings);\n      },\n    ));\n  }\n}\n"
  },
  {
    "path": "lib/utils/num.dart",
    "content": "//hex ---> int\nint hexToInt(String hex) {\n  int val = 0;\n  int len = hex.length;\n  for (int i = 0; i < len; i++) {\n    int hexDigit = hex.codeUnitAt(i);\n    if (hexDigit >= 48 && hexDigit <= 57) {\n      val += (hexDigit - 48) * (1 << (4 * (len - 1 - i)));\n    } else if (hexDigit >= 65 && hexDigit <= 70) {\n      // A..F\n      val += (hexDigit - 55) * (1 << (4 * (len - 1 - i)));\n    } else if (hexDigit >= 97 && hexDigit <= 102) {\n      // a..f\n      val += (hexDigit - 87) * (1 << (4 * (len - 1 - i)));\n    } else {\n      throw FormatException(\"Invalid hexadecimal value $hex\");\n    }\n  }\n  return val;\n}\n\n//int ---> hex\nString intToHex(int i) {\n  return i.toRadixString(16).padLeft(2, '0');\n}\n"
  },
  {
    "path": "lib/utils/platform.dart",
    "content": "import 'dart:io';\n\nimport 'package:device_info_plus/device_info_plus.dart';\n\nclass Platforms {\n  /// 判断是否是桌面端\n  static bool isDesktop() {\n    return Platform.isWindows || Platform.isMacOS || Platform.isLinux;\n  }\n\n  /// 判断是否是移动端\n  static bool isMobile() {\n    return Platform.isAndroid || Platform.isIOS;\n  }\n\n  /// 判断是否是ipad\n  static Future<bool> isIpad() async {\n    if (Platform.isIOS) {\n      final deviceInfo = DeviceInfoPlugin();\n      final iosInfo = await deviceInfo.iosInfo;\n      return iosInfo.model.toLowerCase().contains('ipad');\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "lib/utils/python.dart",
    "content": "import 'package:proxypin/network/http/constants.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/utils/lang.dart';\n\n// 复制为 Python Requests 请求\nString copyAsPythonRequests(HttpRequest request) {\n  var sb = StringBuffer();\n  sb.writeln(\"import requests\\n\");\n\n  String url = request.requestUrl;\n  List<String> headers = request.headers.entries\n      .where((entry) => entry.key.toLowerCase() != 'content-length')\n      .map((entry) => '${entry.key}: ${entry.value}')\n      .toList();\n  String method = request.method.name.toLowerCase();\n\n  sb.write('url = \"${escapeCharacter(url)}\"\\n');\n  bool cookiesExist = processCookies(sb, headers);\n  sb.write('headers = {');\n  processHeaders(sb, headers);\n  sb.writeln('}');\n\n  String? body = processBody(request);\n  if (body != null) {\n    sb.writeln(body);\n  }\n\n  sb.write('\\nres = requests.$method(url, headers=headers');\n  if (cookiesExist) {\n    sb.write(', cookies=cookies');\n  }\n  if (body != null) {\n    sb.write(', data=data');\n  }\n  sb.writeln(')');\n  sb.writeln('print(res.text)');\n\n  return sb.toString();\n}\n\n// 特殊字符转义\nString escapeCharacter(String input) {\n  return input.replaceAll('\\\\', '\\\\\\\\').replaceAll('\"', '\\\\\"').replaceAll(\"'\", \"\\\\'\");\n}\n\n// 处理 cookie\nbool processCookies(StringBuffer py, List<String> headers) {\n  bool cookiesExist = false;\n  for (String header in headers) {\n    if (header.toLowerCase().startsWith(\"cookie:\")) {\n      py.write('cookies = {\\n');\n      var cookies = header.substring(9, header.length - 1).trim().split(';');\n      for (var cookie in cookies) {\n        var parts = cookie.splitFirst('='.codeUnitAt(0));\n        if (parts.length == 2) {\n          py.writeln('  \"${parts[0].trim()}\": \"${escapeCharacter(parts[1].trim())}\",');\n        }\n      }\n      py.writeln('}\\n');\n      cookiesExist = true;\n      break;\n    }\n  }\n  return cookiesExist;\n}\n\n// 处理header\nvoid processHeaders(StringBuffer py, List<String> headers) {\n  bool first = true;\n  for (String header in headers) {\n    if (!header.toLowerCase().startsWith(\"cookie:\")) {\n      if (!first) {\n        py.write(',\\n  ');\n      } else {\n        py.write('\\n  ');\n        first = false;\n      }\n      var parts = header.splitFirst(HttpConstants.colon);\n      py.write('\"${parts[0].trim()}\": \"${escapeCharacter(parts[1].substring(1, parts[1].length - 1).trim())}\"');\n    }\n  }\n  if (!first) {\n    py.write('\\n');\n  }\n}\n\n// 处理body\nString? processBody(HttpRequest request) {\n  if (request.body?.isNotEmpty == true) {\n    return 'data = \"\"\"${escapeCharacter(request.bodyAsString)}\"\"\"';\n  }\n  return null;\n}\n"
  },
  {
    "path": "lib/utils/task.dart",
    "content": "import 'dart:async';\n\n/// 延时任务工具类\nclass DelayedTask {\n  // 私有构造函数，实现单例\n  DelayedTask._internal();\n\n  static final DelayedTask _instance = DelayedTask._internal();\n\n  factory DelayedTask() => _instance;\n\n  // 维护一个任务池，支持同时管理多个不同的延时任务\n  final Map<String, Timer> _taskPool = {};\n\n  /// 执行防抖任务 (Debounce)\n  /// 如果在 [duration] 时间内再次调用相同 [tag] 的任务，前一个任务会被自动取消\n  void debounce(\n    String tag,\n    Duration duration,\n    void Function() action,\n  ) {\n    // 1. 如果旧任务还在运行，直接取消\n    _taskPool[tag]?.cancel();\n\n    // 2. 开启新任务\n    _taskPool[tag] = Timer(duration, () {\n      action();\n      _taskPool.remove(tag); // 执行完毕后移除\n    });\n  }\n\n  /// 延迟 [duration] 后执行一次，返回可手动取消的 Timer\n  /// 适用于不需要防抖，但需要精准手动控制取消的场景\n  Timer delay(Duration duration, void Function() action) {\n    return Timer(duration, action);\n  }\n\n  /// 取消特定标签的任务\n  void cancel(String tag) {\n    if (_taskPool.containsKey(tag)) {\n      _taskPool[tag]?.cancel();\n      _taskPool.remove(tag);\n    }\n  }\n\n  /// 取消所有正在运行的任务 (通常在 dispose 时调用)\n  void cancelAll() {\n    _taskPool.forEach((tag, timer) => timer.cancel());\n    _taskPool.clear();\n  }\n}\n"
  },
  {
    "path": "linux/.gitignore",
    "content": "flutter/ephemeral\n"
  },
  {
    "path": "linux/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"ProxyPin\")\n# The unique GTK application identifier for this application. See:\n# https://wiki.gnome.org/HowDoI/ChooseApplicationID\nset(APPLICATION_ID \"com.network.proxy\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(SET CMP0063 NEW)\n\n# Load bundled libraries from the lib/ directory relative to the binary.\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Root filesystem for cross-building.\nif(FLUTTER_TARGET_PLATFORM_SYSROOT)\n  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\n  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nendif()\n\n# Define build configuration options.\nif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n  set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n    STRING \"Flutter build mode\" FORCE)\n  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n    \"Debug\" \"Profile\" \"Release\")\nendif()\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_14)\n  target_compile_options(${TARGET} PRIVATE -Wall -Werror)\n  target_compile_options(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:-O3>\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:NDEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\n\nadd_definitions(-DAPPLICATION_ID=\"${APPLICATION_ID}\")\n\n# Define the application target. To change its name, change BINARY_NAME above,\n# not the value here, or `flutter run` will no longer work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME}\n  \"main.cc\"\n  \"my_application.cc\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add dependency libraries. Add any application-specific dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter)\ntarget_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n\n# Only the install-generated bundle's copy of the executable will launch\n# correctly, since the resources must in the right relative locations. To avoid\n# people trying to run the unbundled copy, put it in a subdirectory instead of\n# the default top-level location.\nset_target_properties(${BINARY_NAME}\n  PROPERTIES\n  RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/intermediates_do_not_run\"\n)\n\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# By default, \"installing\" just makes a relocatable bundle in the build\n# directory.\nset(BUILD_BUNDLE_DIR \"${PROJECT_BINARY_DIR}/bundle\")\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\n# Start with a clean build bundle directory every time.\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${BUILD_BUNDLE_DIR}/\\\")\n  \" COMPONENT Runtime)\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}/lib\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nforeach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})\n  install(FILES \"${bundled_library}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendforeach(bundled_library)\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\nif(NOT CMAKE_BUILD_TYPE MATCHES \"Debug\")\n  install(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n"
  },
  {
    "path": "linux/build.sh",
    "content": "#!/bin/bash\n\npwd\ncd ../build/linux/x64/release\nrm -rf package\nmkdir -p package/DEBIAN\necho \"Package: ProxyPin\" >> package/DEBIAN/control\necho \"Version: 1.2.6\" >> package/DEBIAN/control\necho \"Priority: optional\" >> package/DEBIAN/control\necho \"Architecture: amd64\" >> package/DEBIAN/control\necho \"Depends: ca-certificates\" >> package/DEBIAN/control\necho \"Section: utils\" >> package/DEBIAN/control\necho \"Maintainer: wanghongenpin@gmail.com\" >> package/DEBIAN/control\necho \"Homepage: https://github.com/wanghongenpin/proxypin\" >> package/DEBIAN/control\necho \"Description: http/https Capture packets\" >> package/DEBIAN/control\necho \"\" >> package/DEBIAN/control\nmkdir -p package/usr/share/applications\ncp ../../../../linux/proxy-pin.desktop package/usr/share/applications\nmkdir package/opt\ncp -r bundle package/opt/proxypin\n\ndpkg -b package ProxyPin-Linux.deb\n"
  },
  {
    "path": "linux/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.10)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\n\n# Serves the same purpose as list(TRANSFORM ... PREPEND ...),\n# which isn't available in 3.10.\nfunction(list_prepend LIST_NAME PREFIX)\n    set(NEW_LIST \"\")\n    foreach(element ${${LIST_NAME}})\n        list(APPEND NEW_LIST \"${PREFIX}${element}\")\n    endforeach(element)\n    set(${LIST_NAME} \"${NEW_LIST}\" PARENT_SCOPE)\nendfunction()\n\n# === Flutter Library ===\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\npkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)\npkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)\n\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/libflutter_linux_gtk.so\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/lib/libapp.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"fl_basic_message_channel.h\"\n  \"fl_binary_codec.h\"\n  \"fl_binary_messenger.h\"\n  \"fl_dart_project.h\"\n  \"fl_engine.h\"\n  \"fl_json_message_codec.h\"\n  \"fl_json_method_codec.h\"\n  \"fl_message_codec.h\"\n  \"fl_method_call.h\"\n  \"fl_method_channel.h\"\n  \"fl_method_codec.h\"\n  \"fl_method_response.h\"\n  \"fl_plugin_registrar.h\"\n  \"fl_plugin_registry.h\"\n  \"fl_standard_message_codec.h\"\n  \"fl_standard_method_codec.h\"\n  \"fl_string_codec.h\"\n  \"fl_value.h\"\n  \"fl_view.h\"\n  \"flutter_linux.h\"\n)\nlist_prepend(FLUTTER_LIBRARY_HEADERS \"${EPHEMERAL_DIR}/flutter_linux/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}\")\ntarget_link_libraries(flutter INTERFACE\n  PkgConfig::GTK\n  PkgConfig::GLIB\n  PkgConfig::GIO\n)\nadd_dependencies(flutter flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CMAKE_CURRENT_BINARY_DIR}/_phony_\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh\"\n      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n)\n"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.cc",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <desktop_multi_window/desktop_multi_window_plugin.h>\n#include <flutter_desktop_context_menu/flutter_desktop_context_menu_plugin.h>\n#include <flutter_js/flutter_js_plugin.h>\n#include <proxy_manager/proxy_manager_plugin.h>\n#include <screen_retriever_linux/screen_retriever_linux_plugin.h>\n#include <url_launcher_linux/url_launcher_plugin.h>\n#include <window_manager/window_manager_plugin.h>\n#include <zstandard_linux/zstandard_linux_plugin.h>\n\nvoid fl_register_plugins(FlPluginRegistry* registry) {\n  g_autoptr(FlPluginRegistrar) desktop_multi_window_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"DesktopMultiWindowPlugin\");\n  desktop_multi_window_plugin_register_with_registrar(desktop_multi_window_registrar);\n  g_autoptr(FlPluginRegistrar) flutter_desktop_context_menu_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"FlutterDesktopContextMenuPlugin\");\n  flutter_desktop_context_menu_plugin_register_with_registrar(flutter_desktop_context_menu_registrar);\n  g_autoptr(FlPluginRegistrar) flutter_js_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"FlutterJsPlugin\");\n  flutter_js_plugin_register_with_registrar(flutter_js_registrar);\n  g_autoptr(FlPluginRegistrar) proxy_manager_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"ProxyManagerPlugin\");\n  proxy_manager_plugin_register_with_registrar(proxy_manager_registrar);\n  g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"ScreenRetrieverLinuxPlugin\");\n  screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);\n  g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"UrlLauncherPlugin\");\n  url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);\n  g_autoptr(FlPluginRegistrar) window_manager_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"WindowManagerPlugin\");\n  window_manager_plugin_register_with_registrar(window_manager_registrar);\n  g_autoptr(FlPluginRegistrar) zstandard_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"ZstandardLinuxPlugin\");\n  zstandard_linux_plugin_register_with_registrar(zstandard_linux_registrar);\n}\n"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.h",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUGIN_REGISTRANT_\n\n#include <flutter_linux/flutter_linux.h>\n\n// Registers Flutter plugins.\nvoid fl_register_plugins(FlPluginRegistry* registry);\n\n#endif  // GENERATED_PLUGIN_REGISTRANT_\n"
  },
  {
    "path": "linux/flutter/generated_plugins.cmake",
    "content": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  desktop_multi_window\n  flutter_desktop_context_menu\n  flutter_js\n  proxy_manager\n  screen_retriever_linux\n  url_launcher_linux\n  window_manager\n  zstandard_linux\n)\n\nlist(APPEND FLUTTER_FFI_PLUGIN_LIST\n)\n\nset(PLUGIN_BUNDLED_LIBRARIES)\n\nforeach(plugin ${FLUTTER_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})\n  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})\nendforeach(plugin)\n\nforeach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})\nendforeach(ffi_plugin)\n"
  },
  {
    "path": "linux/main.cc",
    "content": "#include \"my_application.h\"\n\nint main(int argc, char** argv) {\n  g_autoptr(MyApplication) app = my_application_new();\n  return g_application_run(G_APPLICATION(app), argc, argv);\n}\n"
  },
  {
    "path": "linux/my_application.cc",
    "content": "#include \"my_application.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#ifdef GDK_WINDOWING_X11\n#include <gdk/gdkx.h>\n#endif\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nstruct _MyApplication {\n  GtkApplication parent_instance;\n  char** dart_entrypoint_arguments;\n};\n\nG_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)\n\n// Implements GApplication::activate.\nstatic void my_application_activate(GApplication* application) {\n  MyApplication* self = MY_APPLICATION(application);\n  GtkWindow* window =\n      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));\n\n  // Use a header bar when running in GNOME as this is the common style used\n  // by applications and is the setup most users will be using (e.g. Ubuntu\n  // desktop).\n  // If running on X and not using GNOME then just use a traditional title bar\n  // in case the window manager does more exotic layout, e.g. tiling.\n  // If running on Wayland assume the header bar will work (may need changing\n  // if future cases occur).\n  gboolean use_header_bar = TRUE;\n#ifdef GDK_WINDOWING_X11\n  GdkScreen* screen = gtk_window_get_screen(window);\n  if (GDK_IS_X11_SCREEN(screen)) {\n    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);\n    if (g_strcmp0(wm_name, \"GNOME Shell\") != 0) {\n      use_header_bar = FALSE;\n    }\n  }\n#endif\n  if (use_header_bar) {\n    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());\n    gtk_widget_show(GTK_WIDGET(header_bar));\n    gtk_header_bar_set_title(header_bar, \"ProxyPin\");\n    gtk_header_bar_set_show_close_button(header_bar, TRUE);\n    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));\n  } else {\n    gtk_window_set_title(window, \"ProxyPin\");\n  }\n\n  gtk_window_set_default_size(window, 1280, 720);\n  gtk_widget_show(GTK_WIDGET(window));\n\n  if (g_file_test(\"assets\", G_FILE_TEST_IS_DIR)) {\n      gtk_window_set_icon_from_file(window, \"assets/icon.png\", NULL); // For debug mode\n  } else {\n      gtk_window_set_icon_from_file(window, \"/opt/proxypin/data/flutter_assets/assets/icon.png\", NULL); // For release mode\n  }\n\n  g_autoptr(FlDartProject) project = fl_dart_project_new();\n  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);\n\n  FlView* view = fl_view_new(project);\n  gtk_widget_show(GTK_WIDGET(view));\n  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));\n\n  fl_register_plugins(FL_PLUGIN_REGISTRY(view));\n\n  gtk_widget_grab_focus(GTK_WIDGET(view));\n}\n\n// Implements GApplication::local_command_line.\nstatic gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {\n  MyApplication* self = MY_APPLICATION(application);\n  // Strip out the first argument as it is the binary name.\n  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);\n\n  g_autoptr(GError) error = nullptr;\n  if (!g_application_register(application, nullptr, &error)) {\n     g_warning(\"Failed to register: %s\", error->message);\n     *exit_status = 1;\n     return TRUE;\n  }\n\n  g_application_activate(application);\n  *exit_status = 0;\n\n  return TRUE;\n}\n\n// Implements GObject::dispose.\nstatic void my_application_dispose(GObject* object) {\n  MyApplication* self = MY_APPLICATION(object);\n  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);\n  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);\n}\n\nstatic void my_application_class_init(MyApplicationClass* klass) {\n  G_APPLICATION_CLASS(klass)->activate = my_application_activate;\n  G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;\n  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;\n}\n\nstatic void my_application_init(MyApplication* self) {}\n\nMyApplication* my_application_new() {\n  return MY_APPLICATION(g_object_new(my_application_get_type(),\n                                     \"application-id\", APPLICATION_ID,\n                                     \"flags\", G_APPLICATION_NON_UNIQUE,\n                                     nullptr));\n}\n"
  },
  {
    "path": "linux/my_application.h",
    "content": "#ifndef FLUTTER_MY_APPLICATION_H_\n#define FLUTTER_MY_APPLICATION_H_\n\n#include <gtk/gtk.h>\n\nG_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,\n                     GtkApplication)\n\n/**\n * my_application_new:\n *\n * Creates a new Flutter-based application.\n *\n * Returns: a new #MyApplication.\n */\nMyApplication* my_application_new();\n\n#endif  // FLUTTER_MY_APPLICATION_H_\n"
  },
  {
    "path": "linux/proxy-pin.desktop",
    "content": "[Desktop Entry]\nName=ProxyPin\nGenericName=ProxyPin\nExec=/opt/proxypin/ProxyPin\nIcon=/opt/proxypin/data/flutter_assets/assets/icon.png\nTerminal=false\nType=Application\nCategories=Development;\n"
  },
  {
    "path": "macos/.gitignore",
    "content": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/dgph\n**/xcuserdata/\n"
  },
  {
    "path": "macos/Flutter/Flutter-Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "macos/Flutter/Flutter-Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "macos/Flutter/GeneratedPluginRegistrant.swift",
    "content": "//\n//  Generated file. Do not edit.\n//\n\nimport FlutterMacOS\nimport Foundation\n\nimport desktop_multi_window\nimport device_info_plus\nimport file_picker\nimport flutter_desktop_context_menu\nimport flutter_js\nimport path_provider_foundation\nimport proxy_manager\nimport screen_retriever_macos\nimport share_plus\nimport shared_preferences_foundation\nimport url_launcher_macos\nimport window_manager\nimport zstandard_macos\n\nfunc RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {\n  FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: \"FlutterMultiWindowPlugin\"))\n  DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: \"DeviceInfoPlusMacosPlugin\"))\n  FilePickerPlugin.register(with: registry.registrar(forPlugin: \"FilePickerPlugin\"))\n  FlutterDesktopContextMenuPlugin.register(with: registry.registrar(forPlugin: \"FlutterDesktopContextMenuPlugin\"))\n  FlutterJsPlugin.register(with: registry.registrar(forPlugin: \"FlutterJsPlugin\"))\n  PathProviderPlugin.register(with: registry.registrar(forPlugin: \"PathProviderPlugin\"))\n  ProxyManagerPlugin.register(with: registry.registrar(forPlugin: \"ProxyManagerPlugin\"))\n  ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: \"ScreenRetrieverMacosPlugin\"))\n  SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: \"SharePlusMacosPlugin\"))\n  SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: \"SharedPreferencesPlugin\"))\n  UrlLauncherPlugin.register(with: registry.registrar(forPlugin: \"UrlLauncherPlugin\"))\n  WindowManagerPlugin.register(with: registry.registrar(forPlugin: \"WindowManagerPlugin\"))\n  ZstandardMacosPlugin.register(with: registry.registrar(forPlugin: \"ZstandardMacosPlugin\"))\n}\n"
  },
  {
    "path": "macos/Podfile",
    "content": "platform :osx, '10.15'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \\\"flutter pub get\\\" is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \\\"flutter pub get\\\"\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_macos_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))\n  target 'RunnerTests' do\n    inherit! :search_paths\n  end\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_macos_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "macos/Runner/AppDelegate.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\n@main\nclass AppDelegate: FlutterAppDelegate {\n    \n    override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n        return true\n    }\n    \n    override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {\n        if !flag {\n            for window in NSApp.windows {\n                if !window.isVisible {\n                    window.setIsVisible(true)\n                }\n                window.makeKeyAndOrderFront(self)\n                NSApp.activate(ignoringOtherApps: true)\n            }\n        }\n        return true\n    }\n    \n    override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {\n      return true\n    }\n\n    override func applicationWillTerminate(_ notification: Notification) {\n        AppLifecycleChannel.appDetached()\n        NSLog(\"applicationWillTerminate\")\n    }\n    \n}\n"
  },
  {
    "path": "macos/Runner/AppLifecycleChannel.swift",
    "content": "//\n//  AppLifecycleChannel.swift\n//\n//  Created by wanghongen on 2023/12/21.\n//\n\nimport Foundation\nimport FlutterMacOS\n\nclass AppLifecycleChannel {\n    static private var channel : FlutterMethodChannel?\n    \n    //注册\n    static func registerChannel(flutterViewController: FlutterViewController) {\n        channel = FlutterMethodChannel(name: \"com.proxy/appLifecycle\", binaryMessenger: flutterViewController.engine.binaryMessenger)\n    }\n    \n    static func appDetached()  {\n        channel!.invokeMethod(\"appDetached\", arguments: nil)\n        Thread.sleep(forTimeInterval: 0.5)\n    }\n}\n"
  },
  {
    "path": "macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n    \"images\": [\n        {\n            \"size\": \"16x16\",\n            \"idiom\": \"mac\",\n            \"filename\": \"icon-16.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"16x16\",\n            \"idiom\": \"mac\",\n            \"filename\": \"icon-16@2x.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"32x32\",\n            \"idiom\": \"mac\",\n            \"filename\": \"icon-32.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"32x32\",\n            \"idiom\": \"mac\",\n            \"filename\": \"icon-32@2x.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"128x128\",\n            \"idiom\": \"mac\",\n            \"filename\": \"icon-128.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"128x128\",\n            \"idiom\": \"mac\",\n            \"filename\": \"icon-128@2x.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"256x256\",\n            \"idiom\": \"mac\",\n            \"filename\": \"icon-256.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"256x256\",\n            \"idiom\": \"mac\",\n            \"filename\": \"icon-256@2x.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"512x512\",\n            \"idiom\": \"mac\",\n            \"filename\": \"icon-512.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"512x512\",\n            \"idiom\": \"mac\",\n            \"filename\": \"icon-512@2x.png\",\n            \"scale\": \"2x\"\n        }\n    ],\n    \"info\": {\n        \"version\": 1,\n        \"author\": \"icon.wuruihong.com\"\n    }\n}"
  },
  {
    "path": "macos/Runner/Base.lproj/MainMenu.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion=\"21507\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"21507\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <objects>\n        <customObject id=\"-2\" userLabel=\"File's Owner\" customClass=\"NSApplication\">\n            <connections>\n                <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"GzC-gU-4Uq\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"-1\" userLabel=\"First Responder\" customClass=\"FirstResponder\"/>\n        <customObject id=\"-3\" userLabel=\"Application\" customClass=\"NSObject\"/>\n        <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModule=\"ProxyPin\" customModuleProvider=\"target\">\n            <connections>\n                <outlet property=\"applicationMenu\" destination=\"uQy-DD-JDr\" id=\"XBo-yE-nKs\"/>\n                <outlet property=\"mainFlutterWindow\" destination=\"QvC-M9-y7g\" id=\"gIp-Ho-8D9\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n        <menu title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n            <items>\n                <menuItem title=\"APP_NAME\" id=\"1Xt-HY-uBw\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"APP_NAME\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                        <items>\n                            <menuItem title=\"About APP_NAME\" id=\"5kV-Vb-QxS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"orderFrontStandardAboutPanel:\" target=\"-1\" id=\"Exp-CZ-Vem\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                            <menuItem title=\"Preferences…\" keyEquivalent=\",\" id=\"BOF-NM-1cW\"/>\n                            <menuItem isSeparatorItem=\"YES\" id=\"wFC-TO-SCJ\"/>\n                            <menuItem title=\"Services\" id=\"NMo-om-nkz\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Services\" systemMenu=\"services\" id=\"hz9-B4-Xy5\"/>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"4je-JR-u6R\"/>\n                            <menuItem title=\"Hide APP_NAME\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                <connections>\n                                    <action selector=\"hide:\" target=\"-1\" id=\"PnN-Uc-m68\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"hideOtherApplications:\" target=\"-1\" id=\"VT4-aY-XCT\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"unhideAllApplications:\" target=\"-1\" id=\"Dhg-Le-xox\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Edit\" id=\"5QF-Oa-p0T\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Edit\" id=\"W48-6f-4Dl\">\n                        <items>\n                            <menuItem title=\"Undo\" keyEquivalent=\"z\" id=\"dRJ-4n-Yzg\">\n                                <connections>\n                                    <action selector=\"undo:\" target=\"-1\" id=\"M6e-cu-g7V\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Redo\" keyEquivalent=\"Z\" id=\"6dh-zS-Vam\">\n                                <connections>\n                                    <action selector=\"redo:\" target=\"-1\" id=\"oIA-Rs-6OD\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"WRV-NI-Exz\"/>\n                            <menuItem title=\"Cut\" keyEquivalent=\"x\" id=\"uRl-iY-unG\">\n                                <connections>\n                                    <action selector=\"cut:\" target=\"-1\" id=\"YJe-68-I9s\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Copy\" keyEquivalent=\"c\" id=\"x3v-GG-iWU\">\n                                <connections>\n                                    <action selector=\"copy:\" target=\"-1\" id=\"G1f-GL-Joy\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste\" keyEquivalent=\"v\" id=\"gVA-U4-sdL\">\n                                <connections>\n                                    <action selector=\"paste:\" target=\"-1\" id=\"UvS-8e-Qdg\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste and Match Style\" keyEquivalent=\"V\" id=\"WeT-3V-zwk\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"pasteAsPlainText:\" target=\"-1\" id=\"cEh-KX-wJQ\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Delete\" id=\"pa3-QI-u2k\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"delete:\" target=\"-1\" id=\"0Mk-Ml-PaM\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Select All\" keyEquivalent=\"a\" id=\"Ruw-6m-B2m\">\n                                <connections>\n                                    <action selector=\"selectAll:\" target=\"-1\" id=\"VNm-Mi-diN\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"uyl-h8-XO2\"/>\n                            <menuItem title=\"Find\" id=\"4EN-yA-p0u\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Find\" id=\"1b7-l0-nxx\">\n                                    <items>\n                                        <menuItem title=\"Find…\" tag=\"1\" keyEquivalent=\"f\" id=\"Xz5-n4-O0W\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"cD7-Qs-BN4\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find and Replace…\" tag=\"12\" keyEquivalent=\"f\" id=\"YEy-JH-Tfz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"WD3-Gg-5AJ\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Next\" tag=\"2\" keyEquivalent=\"g\" id=\"q09-fT-Sye\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"NDo-RZ-v9R\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Previous\" tag=\"3\" keyEquivalent=\"G\" id=\"OwM-mh-QMV\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"HOh-sY-3ay\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Use Selection for Find\" tag=\"7\" keyEquivalent=\"e\" id=\"buJ-ug-pKt\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"U76-nv-p5D\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Jump to Selection\" keyEquivalent=\"j\" id=\"S0p-oC-mLd\">\n                                            <connections>\n                                                <action selector=\"centerSelectionInVisibleArea:\" target=\"-1\" id=\"IOG-6D-g5B\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Spelling and Grammar\" id=\"Dv1-io-Yv7\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Spelling\" id=\"3IN-sU-3Bg\">\n                                    <items>\n                                        <menuItem title=\"Show Spelling and Grammar\" keyEquivalent=\":\" id=\"HFo-cy-zxI\">\n                                            <connections>\n                                                <action selector=\"showGuessPanel:\" target=\"-1\" id=\"vFj-Ks-hy3\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Document Now\" keyEquivalent=\";\" id=\"hz2-CU-CR7\">\n                                            <connections>\n                                                <action selector=\"checkSpelling:\" target=\"-1\" id=\"fz7-VC-reM\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"bNw-od-mp5\"/>\n                                        <menuItem title=\"Check Spelling While Typing\" id=\"rbD-Rh-wIN\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleContinuousSpellChecking:\" target=\"-1\" id=\"7w6-Qz-0kB\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Grammar With Spelling\" id=\"mK6-2p-4JG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleGrammarChecking:\" target=\"-1\" id=\"muD-Qn-j4w\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Correct Spelling Automatically\" id=\"78Y-hA-62v\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticSpellingCorrection:\" target=\"-1\" id=\"2lM-Qi-WAP\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Substitutions\" id=\"9ic-FL-obx\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Substitutions\" id=\"FeM-D8-WVr\">\n                                    <items>\n                                        <menuItem title=\"Show Substitutions\" id=\"z6F-FW-3nz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontSubstitutionsPanel:\" target=\"-1\" id=\"oku-mr-iSq\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"gPx-C9-uUO\"/>\n                                        <menuItem title=\"Smart Copy/Paste\" id=\"9yt-4B-nSM\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleSmartInsertDelete:\" target=\"-1\" id=\"3IJ-Se-DZD\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Quotes\" id=\"hQb-2v-fYv\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticQuoteSubstitution:\" target=\"-1\" id=\"ptq-xd-QOA\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Dashes\" id=\"rgM-f4-ycn\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDashSubstitution:\" target=\"-1\" id=\"oCt-pO-9gS\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Links\" id=\"cwL-P1-jid\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticLinkDetection:\" target=\"-1\" id=\"Gip-E3-Fov\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Data Detectors\" id=\"tRr-pd-1PS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDataDetection:\" target=\"-1\" id=\"R1I-Nq-Kbl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Text Replacement\" id=\"HFQ-gK-NFA\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticTextReplacement:\" target=\"-1\" id=\"DvP-Fe-Py6\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Transformations\" id=\"2oI-Rn-ZJC\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Transformations\" id=\"c8a-y6-VQd\">\n                                    <items>\n                                        <menuItem title=\"Make Upper Case\" id=\"vmV-6d-7jI\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"uppercaseWord:\" target=\"-1\" id=\"sPh-Tk-edu\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Make Lower Case\" id=\"d9M-CD-aMd\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"lowercaseWord:\" target=\"-1\" id=\"iUZ-b5-hil\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Capitalize\" id=\"UEZ-Bs-lqG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"capitalizeWord:\" target=\"-1\" id=\"26H-TL-nsh\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Speech\" id=\"xrE-MZ-jX0\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Speech\" id=\"3rS-ZA-NoH\">\n                                    <items>\n                                        <menuItem title=\"Start Speaking\" id=\"Ynk-f8-cLZ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"startSpeaking:\" target=\"-1\" id=\"654-Ng-kyl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Stop Speaking\" id=\"Oyz-dy-DGm\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"stopSpeaking:\" target=\"-1\" id=\"dX8-6p-jy9\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"View\" id=\"H8h-7b-M4v\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"View\" id=\"HyV-fh-RgO\">\n                        <items>\n                            <menuItem title=\"Enter Full Screen\" keyEquivalent=\"f\" id=\"4J7-dP-txa\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"toggleFullScreen:\" target=\"-1\" id=\"dU3-MA-1Rq\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Window\" id=\"aUF-d1-5bR\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Window\" systemMenu=\"window\" id=\"Td7-aD-5lo\">\n                        <items>\n                            <menuItem title=\"Minimize\" keyEquivalent=\"m\" id=\"OY7-WF-poV\">\n                                <connections>\n                                    <action selector=\"performMiniaturize:\" target=\"-1\" id=\"VwT-WD-YPe\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Zoom\" id=\"R4o-n2-Eq4\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"performZoom:\" target=\"-1\" id=\"DIl-cC-cCs\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"eu3-7i-yIM\"/>\n                            <menuItem title=\"Bring All to Front\" id=\"LE2-aR-0XJ\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"arrangeInFront:\" target=\"-1\" id=\"DRN-fu-gQh\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Help\" id=\"EPT-qC-fAb\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Help\" systemMenu=\"help\" id=\"rJ0-wn-3NY\"/>\n                </menuItem>\n            </items>\n            <point key=\"canvasLocation\" x=\"142\" y=\"-258\"/>\n        </menu>\n        <window title=\"APP_NAME\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" id=\"QvC-M9-y7g\" customClass=\"MainFlutterWindow\" customModule=\"ProxyPin\" customModuleProvider=\"target\">\n            <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n            <rect key=\"contentRect\" x=\"335\" y=\"390\" width=\"800\" height=\"600\"/>\n            <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"1680\" height=\"1025\"/>\n            <view key=\"contentView\" wantsLayer=\"YES\" id=\"EiT-Mj-1SZ\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"800\" height=\"600\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n            </view>\n            <point key=\"canvasLocation\" x=\"7\" y=\"-655\"/>\n        </window>\n    </objects>\n</document>\n"
  },
  {
    "path": "macos/Runner/Configs/AppInfo.xcconfig",
    "content": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the\n// future. If not, the values below would default to using the project name when this becomes a\n// 'flutter create' template.\n\n// The application's name. By default this is also the title of the Flutter window.\nPRODUCT_NAME = ProxyPin\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2023 Hongen Wang. All rights reserved.\n"
  },
  {
    "path": "macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Warnings.xcconfig",
    "content": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings\nGCC_WARN_UNDECLARED_SELECTOR = YES\nCLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES\nCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE\nCLANG_WARN__DUPLICATE_METHOD_MATCH = YES\nCLANG_WARN_PRAGMA_PACK = YES\nCLANG_WARN_STRICT_PROTOTYPES = YES\nCLANG_WARN_COMMA = YES\nGCC_WARN_STRICT_SELECTOR_MATCH = YES\nCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES\nCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES\nGCC_WARN_SHADOW = YES\nCLANG_WARN_UNREACHABLE_CODE = YES\n"
  },
  {
    "path": "macos/Runner/DebugProfile.entitlements",
    "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>com.apple.security.files.user-selected.read-write</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.network.client</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n\t<key>com.apple.security.scripting-targets</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/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>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></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>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>$(PRODUCT_COPYRIGHT)</string>\n\t<key>NSMainNibFile</key>\n\t<string>MainMenu</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n    <key>LSApplicationCategoryType</key>\n    <string>public.app-category.utilities</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\nclass MainFlutterWindow: NSWindow {\n    // Offsets from the top-left corner of the title bar container (in points)\n    // Tweak these to your preferred locations.\n    private let closeButtonTopLeftOffset = NSPoint(x: 10, y: 13)\n    private let miniaturizeButtonTopLeftOffset = NSPoint(x: 32, y: 13)\n    private let zoomButtonTopLeftOffset = NSPoint(x: 52, y: 13)\n\n    // Whether to auto-apply on window events\n    private let shouldAutoApplyTrafficLightPositions = true\n    private var trafficLightObserversRegistered = false\n\n    override func awakeFromNib() {\n        let flutterViewController = FlutterViewController()\n        let windowFrame = self.frame\n        self.contentViewController = flutterViewController\n        self.setFrame(windowFrame, display: true)\n\n        AppLifecycleChannel.registerChannel(flutterViewController: flutterViewController)\n\n        RegisterGeneratedPlugins(registry: flutterViewController)\n\n        super.awakeFromNib()\n\n        // Apply custom positions for traffic-light buttons and observe changes\n        if shouldAutoApplyTrafficLightPositions {\n            applyTrafficLightButtonPositions()\n            registerWindowObservers()\n        }\n    }\n\n    deinit {\n        unregisterWindowObservers()\n    }\n\n    // MARK: - Traffic-light buttons positioning\n\n    private func applyTrafficLightButtonPositions() {\n        // Skip in full screen; macOS manages buttons differently there\n        if self.styleMask.contains(.fullScreen) { return }\n\n        setStandardWindowButton(.closeButton, topLeftOffset: closeButtonTopLeftOffset)\n        setStandardWindowButton(.miniaturizeButton, topLeftOffset: miniaturizeButtonTopLeftOffset)\n        setStandardWindowButton(.zoomButton, topLeftOffset: zoomButtonTopLeftOffset)\n    }\n\n    private func setStandardWindowButton(_ type: NSWindow.ButtonType, topLeftOffset: NSPoint) {\n        guard let button = self.standardWindowButton(type), let container = button.superview else { return }\n\n        // Ensure autoresizing mask changes are respected when moving via frames\n        button.translatesAutoresizingMaskIntoConstraints = true\n\n        // Convert a top-left based offset to the container's default bottom-left coordinate system\n        let containerHeight = container.bounds.height\n        let targetY = containerHeight - topLeftOffset.y - button.frame.height\n        let targetX = topLeftOffset.x\n        let newOrigin = NSPoint(x: max(0, targetX), y: max(0, targetY))\n\n        // Avoid redundant layout churn\n        if button.frame.origin.equalTo(newOrigin) { return }\n        button.setFrameOrigin(newOrigin)\n    }\n\n    private func registerWindowObservers() {\n        guard !trafficLightObserversRegistered else { return }\n        trafficLightObserversRegistered = true\n        NotificationCenter.default.addObserver(self, selector: #selector(onWindowDidResize(_:)), name: NSWindow.didResizeNotification, object: self)\n        NotificationCenter.default.addObserver(self, selector: #selector(onWindowDidEndLiveResize(_:)), name: NSWindow.didEndLiveResizeNotification, object: self)\n        NotificationCenter.default.addObserver(self, selector: #selector(onWindowDidBecomeKey(_:)), name: NSWindow.didBecomeKeyNotification, object: self)\n        NotificationCenter.default.addObserver(self, selector: #selector(onWindowDidExitFullScreen(_:)), name: NSWindow.didExitFullScreenNotification, object: self)\n        NotificationCenter.default.addObserver(self, selector: #selector(onWindowDidEnterFullScreen(_:)), name: NSWindow.didEnterFullScreenNotification, object: self)\n    }\n\n    private func unregisterWindowObservers() {\n        if trafficLightObserversRegistered {\n            NotificationCenter.default.removeObserver(self, name: NSWindow.didResizeNotification, object: self)\n            NotificationCenter.default.removeObserver(self, name: NSWindow.didEndLiveResizeNotification, object: self)\n            NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeKeyNotification, object: self)\n            NotificationCenter.default.removeObserver(self, name: NSWindow.didExitFullScreenNotification, object: self)\n            NotificationCenter.default.removeObserver(self, name: NSWindow.didEnterFullScreenNotification, object: self)\n            trafficLightObserversRegistered = false\n        }\n    }\n\n    @objc private func onWindowDidResize(_ notification: Notification) {\n        applyTrafficLightButtonPositions()\n    }\n\n    @objc private func onWindowDidEndLiveResize(_ notification: Notification) {\n        applyTrafficLightButtonPositions()\n    }\n\n    @objc private func onWindowDidBecomeKey(_ notification: Notification) {\n        applyTrafficLightButtonPositions()\n    }\n\n    @objc private func onWindowDidExitFullScreen(_ notification: Notification) {\n        // Re-apply after leaving full screen\n        DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in\n            self?.applyTrafficLightButtonPositions()\n        }\n    }\n\n    @objc private func onWindowDidEnterFullScreen(_ notification: Notification) {\n        // No-op; let system manage buttons\n    }\n}\n"
  },
  {
    "path": "macos/Runner/Release.entitlements",
    "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>com.apple.security.files.user-selected.read-write</key>\n\t<true/>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.network.client</key>\n    <true/>\n    <key>com.apple.security.network.server</key>\n    <true/>\n    <key>com.apple.security.scripting-targets</key>\n    <true/>\n    <key>com.apple.security.files.user-selected.read-write</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/RunnerProfile.entitlements",
    "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>com.apple.security.app-sandbox</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXAggregateTarget section */\n\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {\n\t\t\tisa = PBXAggregateTarget;\n\t\t\tbuildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC111E2044C6BF0003C045 /* ShellScript */,\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Flutter Assemble\";\n\t\t\tproductName = FLX;\n\t\t};\n/* End PBXAggregateTarget section */\n\n/* Begin PBXBuildFile section */\n\t\t331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n\t\t59CD0B3B69B2AD63E8F7FD5B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15B6D3EA3FFF7C54E0E30275 /* Pods_Runner.framework */; };\n\t\t85F740723892A1960748773D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C326C0FC3A1C8C78354E1D3 /* Pods_RunnerTests.framework */; };\n\t\t9B673CF12A383721009CB5B5 /* T.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B673CF02A383721009CB5B5 /* T.m */; };\n\t\t9B8D91812B335386009B90B1 /* AppLifecycleChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8D91802B335386009B90B1 /* AppLifecycleChannel.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC10EC2044A3C60003C045;\n\t\t\tremoteInfo = Runner;\n\t\t};\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t09B2D176FF453AC4820D93B0 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.release.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t15B6D3EA3FFF7C54E0E30275 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* ProxyPin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ProxyPin.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = \"<group>\"; };\n\t\t33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = \"<group>\"; };\n\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = \"<group>\"; };\n\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = \"Flutter-Generated.xcconfig\"; path = \"ephemeral/Flutter-Generated.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = \"<group>\"; };\n\t\t33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = \"<group>\"; };\n\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = \"<group>\"; };\n\t\t45A80AA865CEE57B23D60762 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.debug.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t6C326C0FC3A1C8C78354E1D3 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t8F664CC27DDA6C1AEED215C8 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.debug.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9B673CF02A383721009CB5B5 /* T.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = T.m; sourceTree = \"<group>\"; };\n\t\t9B8D91802B335386009B90B1 /* AppLifecycleChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLifecycleChannel.swift; sourceTree = \"<group>\"; };\n\t\t9BCACE942A3AAED1009FBC53 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = \"<group>\"; };\n\t\tAEE2D821CE41DE974B8482C9 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.profile.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tC037DD715A776D1345BC6EBC /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.release.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tD8C744BDF4702E67F0AD5176 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t331C80D2294CF70F00263BE5 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t85F740723892A1960748773D /* Pods_RunnerTests.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t59CD0B3B69B2AD63E8F7FD5B /* Pods_Runner.framework 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\t24D3DAABC0954B0AB8457F8B /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t45A80AA865CEE57B23D60762 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\tC037DD715A776D1345BC6EBC /* Pods-Runner.release.xcconfig */,\n\t\t\t\tD8C744BDF4702E67F0AD5176 /* Pods-Runner.profile.xcconfig */,\n\t\t\t\t8F664CC27DDA6C1AEED215C8 /* Pods-RunnerTests.debug.xcconfig */,\n\t\t\t\t09B2D176FF453AC4820D93B0 /* Pods-RunnerTests.release.xcconfig */,\n\t\t\t\tAEE2D821CE41DE974B8482C9 /* Pods-RunnerTests.profile.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t331C80D6294CF71000263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t331C80D7294CF71000263BE5 /* RunnerTests.swift */,\n\t\t\t);\n\t\t\tpath = RunnerTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33BA886A226E78AF003329D5 /* Configs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10E42044A3C60003C045 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33FAB671232836740065AC1E /* Runner */,\n\t\t\t\t33CEB47122A05771004F2AC0 /* Flutter */,\n\t\t\t\t331C80D6294CF71000263BE5 /* RunnerTests */,\n\t\t\t\t33CC10EE2044A3C60003C045 /* Products */,\n\t\t\t\tD73912EC22F37F3D000D13A0 /* Frameworks */,\n\t\t\t\t24D3DAABC0954B0AB8457F8B /* Pods */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10EE2044A3C60003C045 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10ED2044A3C60003C045 /* ProxyPin.app */,\n\t\t\t\t331C80D5294CF71000263BE5 /* RunnerTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9B8D91802B335386009B90B1 /* AppLifecycleChannel.swift */,\n\t\t\t\t9BCACE942A3AAED1009FBC53 /* RunnerProfile.entitlements */,\n\t\t\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */,\n\t\t\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,\n\t\t\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */,\n\t\t\t\t33E51914231749380026EE4D /* Release.entitlements */,\n\t\t\t\t33CC11242044D66E0003C045 /* Resources */,\n\t\t\t\t33BA886A226E78AF003329D5 /* Configs */,\n\t\t\t\t9B673CF02A383721009CB5B5 /* T.m */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t15B6D3EA3FFF7C54E0E30275 /* Pods_Runner.framework */,\n\t\t\t\t6C326C0FC3A1C8C78354E1D3 /* Pods_RunnerTests.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t331C80D4294CF70F00263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tC9D0221AA72EA2D4928E496B /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t331C80D1294CF70F00263BE5 /* Sources */,\n\t\t\t\t331C80D2294CF70F00263BE5 /* Frameworks */,\n\t\t\t\t331C80D3294CF70F00263BE5 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C80DA294CF71000263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t409BBA90ED8EA1A164A8FDA1 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t33CC10E92044A3C60003C045 /* Sources */,\n\t\t\t\t33CC10EA2044A3C60003C045 /* Frameworks */,\n\t\t\t\t33CC10EB2044A3C60003C045 /* Resources */,\n\t\t\t\t33CC110E2044A8840003C045 /* Bundle Framework */,\n\t\t\t\t3399D490228B24CF009A79C7 /* ShellScript */,\n\t\t\t\t7A8D05FF293040491B8108BB /* [CP] Embed Pods Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 33CC10ED2044A3C60003C045 /* ProxyPin.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t33CC10E52044A3C60003C045 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C80D4294CF70F00263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tLastSwiftMigration = 1420;\n\t\t\t\t\t\tTestTargetID = 33CC10EC2044A3C60003C045;\n\t\t\t\t\t};\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\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 = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t331C80D4294CF70F00263BE5 /* RunnerTests */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C80D3294CF70F00263BE5 /* 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\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib 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\t3399D490228B24CF009A79C7 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\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);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"echo \\\"$PRODUCT_NAME.app\\\" > \\\"$PROJECT_DIR\\\"/Flutter/ephemeral/.app_filename && \\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh embed\\n\";\n\t\t};\n\t\t33CC111E2044C6BF0003C045 /* ShellScript */ = {\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\tFlutter/ephemeral/FlutterInputs.xcfilelist,\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\tFlutter/ephemeral/tripwire,\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterOutputs.xcfilelist,\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\";\n\t\t};\n\t\t409BBA90ED8EA1A164A8FDA1 /* [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-Runner-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\t7A8D05FF293040491B8108BB /* [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-Runner/Pods-Runner-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-Runner/Pods-Runner-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-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\tC9D0221AA72EA2D4928E496B /* [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-RunnerTests-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/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t331C80D1294CF70F00263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t9B673CF12A383721009CB5B5 /* T.m in Sources */,\n\t\t\t\t331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t9B8D91812B335386009B90B1 /* AppLifecycleChannel.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift 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\t331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC10EC2044A3C60003C045 /* Runner */;\n\t\t\ttargetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;\n\t\t};\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t331C80DB294CF71000263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 8F664CC27DDA6C1AEED215C8 /* Pods-RunnerTests.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\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\tMACOSX_DEPLOYMENT_TARGET = \"$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)\";\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/RunnerTests-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/network.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/network\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C80DC294CF71000263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 09B2D176FF453AC4820D93B0 /* Pods-RunnerTests.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\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\tMACOSX_DEPLOYMENT_TARGET = \"$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)\";\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/RunnerTests-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/network.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/network\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C80DD294CF71000263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = AEE2D821CE41DE974B8482C9 /* Pods-RunnerTests.profile.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\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\tMACOSX_DEPLOYMENT_TARGET = \"$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)\";\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/RunnerTests-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/network.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/network\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\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_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_DOCUMENTATION_COMMENTS = YES;\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_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_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\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_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\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEA231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.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\tCODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEVELOPMENT_TEAM = DM3F8VR243;\n\t\t\t\tENABLE_HARDENED_RUNTIME = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = ProxyPin;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.utilities\";\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\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMARKETING_VERSION = 1.1.8;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEB231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = \"$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t33CC10F92044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\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_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_DOCUMENTATION_COMMENTS = YES;\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_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_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\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_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\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\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FA2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\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_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_DOCUMENTATION_COMMENTS = YES;\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_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_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\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_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\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC10FC2044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.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\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEVELOPMENT_TEAM = DM3F8VR243;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = ProxyPin;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.utilities\";\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\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMARKETING_VERSION = 1.1.8;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FD2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.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\tCODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEVELOPMENT_TEAM = DM3F8VR243;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = ProxyPin;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.utilities\";\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\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMARKETING_VERSION = 1.1.8;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = \"$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC111D2044C6BA0003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = \"$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C80DB294CF71000263BE5 /* Debug */,\n\t\t\t\t331C80DC294CF71000263BE5 /* Release */,\n\t\t\t\t331C80DD294CF71000263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\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 = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.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>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\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 = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"ProxyPin.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.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      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"ProxyPin.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            parallelizable = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"331C80D4294CF70F00263BE5\"\n               BuildableName = \"RunnerTests.xctest\"\n               BlueprintName = \"RunnerTests\"\n               ReferencedContainer = \"container:Runner.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      enableGPUValidationMode = \"1\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"ProxyPin.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"ProxyPin.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.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": "macos/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.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>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/RunnerTests/RunnerTests.swift",
    "content": "import FlutterMacOS\nimport Cocoa\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add code to the Runner application, consider adding tests here.\n    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.\n  }\n\n}\n"
  },
  {
    "path": "macos/packaging/dmg/make_config.yaml",
    "content": "title: ProxyPin\ncontents:\n  - x: 448\n    y: 344\n    type: link\n    path: \"/Applications\"\n  - x: 192\n    y: 344\n    type: file\n    path: ProxyPin.app\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: proxypin\ndescription: ProxyPin\npublish_to: 'none' # Remove this line if you wish to publish to pub.dev\n\nversion: 1.2.6+29\n\nenvironment:\n  sdk: '>=3.0.2 <4.0.0'\n\ndependencies:\n  flutter:\n    sdk: flutter\n  flutter_localizations:\n    sdk: flutter\n  intl: any\n  cupertino_icons: ^1.0.8\n  pointycastle: ^4.0.0\n  logger: ^2.6.2\n  date_format: ^2.0.9\n  window_manager: ^0.5.1\n  desktop_multi_window:\n    git:\n      url: https://gitee.com/wanghongenpin/flutter-plugins.git\n      path: packages/desktop_multi_window\n  path_provider: ^2.1.5\n  file_picker: ^10.3.10\n  proxy_manager: ^0.0.3\n  permission_handler: ^12.0.1\n  flutter_toastr: ^1.0.3\n  share_plus: ^12.0.1\n  flutter_js: ^0.8.7\n  flutter_code_editor:\n    git:\n      url: https://github.com/wanghongenpin/flutter-code-editor.git\n      ref: secure-keyboard\n  flutter_highlight: ^0.7.0\n  flutter_desktop_context_menu: ^0.2.0\n  device_info_plus: ^12.3.0\n  shared_preferences: ^2.5.4\n  url_launcher: ^6.3.2\n  toastification: ^3.0.2\n  get: ^4.7.3\n  zstandard: ^1.3.29\n  qr_flutter: ^4.1.0\n  flutter_qr_reader_plus: ^1.0.6\n  brotli: ^0.6.0\n#  macos_window_utils: 1.6.1\n  win32audio: ^1.3.1\n  vclibs: ^0.1.3\n  scrollable_positioned_list_nic: ^0.0.2\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  flutter_lints: ^6.0.0\n\n# The following section is specific to Flutter packages.\nflutter:\n  generate: true\n  uses-material-design: true\n  assets:\n    - assets/certs/ca.crt\n    - assets/certs/ca_key.pem\n    - assets/icon.png\n    - assets/icon_foreground.png\n    - assets/js/"
  },
  {
    "path": "test/base64_test.dart",
    "content": "import 'dart:convert';\nimport 'dart:typed_data';\n\nvoid main() {\n  print(base64Decode(\"CiRjNjJlOTc0ZC1j\"));\n  print(utf8.decode(base64Decode(\"CiRjNjJlOTc0ZC1j\")));\n  // 输入的十六进制字符串\n  String hex = \"1F 8B 08 00 00 00 00 00 00 FF DD 58 CF\";\n  // 转换为Base64\n  String base64Str = hexToBase64(hex);\n  print(\"转换后的Base64: $base64Str\");\n}\n\nString hexToBase64(String hex) {\n  // 移除十六进制字符串中的空格\n  var arr = hex.split(' ');\n  // 将十六进制字符串转换为字节数组\n  List<int> bytes = [];\n  for (int i = 0; i < arr.length; i ++) {\n    bytes.add(int.parse(arr[i], radix: 16));\n  }\n  print(bytes);\n  // 将字节数组编码为 Base64\n  return base64Encode(Uint8List.fromList(bytes));\n}"
  },
  {
    "path": "test/cert_test.dart",
    "content": "import 'dart:io';\nimport 'dart:math';\n\nimport 'package:pointycastle/asymmetric/api.dart';\nimport 'package:proxypin/network/util/cert/basic_constraints.dart';\nimport 'package:proxypin/network/util/cert/cert_data.dart';\nimport 'package:proxypin/network/util/cert/extension.dart';\nimport 'package:proxypin/network/util/cert/key_usage.dart' as x509;\nimport 'package:proxypin/network/util/cert/x509.dart';\nimport 'package:proxypin/network/util/crypto.dart';\n\nvoid main() async {\n  var caPem = await File('assets/certs/ca.crt').readAsString();\n  //生成 公钥和私钥\n  var caRoot = X509Utils.x509CertificateFromPem(caPem);\n  var generateRSAKeyPair = CryptoUtils.generateRSAKeyPair();\n  var serverPubKey = generateRSAKeyPair.publicKey as RSAPublicKey;\n  // var serverPriKey = generateRSAKeyPair.privateKey as RSAPrivateKey;\n\n  print(CryptoUtils.encodeRSAPublicKeyToPem(serverPubKey));\n  //保存私钥\n  var serverPublicKeyPem = \"\"\"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVXqbCErPZMS+2Eb3MUT\neTNIYZHoCMZk5gFIo3pD70dZimQj2yMBIh9Rq4rO0/Dj9zt52vR1zbxDnmx/5TDC\ndjDHk/zHYW66VLYCo4n1H4/dddFvJ8Y8syBNpa+seSAR6ljF807gZqINGeNKi8Du\nN82XiED2Ix3woE1jMQfP3E16alxHaejFBZ77SUOXJhJDM5SKD2H0bxGw9cVw9K69\nNmnZMIM9+U8+TuM9EzvMUuHTY278Ov72c9HpO5OAx2zfyXGlmUGgyUCiYnxeATX5\nLceGVEoT2MWhFibWvPBpH315xNXU57dWKWW714tPsvzzNHzKZspz/LQ36fU9goUg\nNQIDAQAB\n-----END PUBLIC KEY-----\n  \"\"\";\n  print(serverPublicKeyPem);\n  var readAsString = await File('assets/certs/server.key').readAsString();\n  // var rsaPrivateKeyFromPem = CryptoUtils.rsaPrivateKeyFromPem(serverPriKeyPem);\n  // print(rsaPrivateKeyFromPem);\n  var crt = generate(\n      caRoot, CryptoUtils.rsaPublicKeyFromPem(serverPublicKeyPem), CryptoUtils.rsaPrivateKeyFromPem(readAsString));\n  print(crt);\n\n  // await File('assets/certs/server.crt').writeAsString(crt);\n  // var readAsString2 = File('assets/certs/server.crt').readAsStringSync();\n\n  //TLS服务器证书必须包含ExtendedKeyUsage（EKU）扩展，该扩展包含id-kp-serverAuth OID。\n\n  // X509Utils.generateSelfSignedCertificate(serverPriKey, caPem, 825,\n  //     serialNumber: Random().nextInt(1000000).toString(),\n  //     sans: [\n  //       'ProxyPin CA (${Platform.localHostname})'\n  //     ],\n  //     issuer: {\n  //       'C': 'CN',\n  //       'ST': 'BJ',\n  //       'L': 'Beijing',\n  //       'O': 'Proxy',\n  //       'OU': 'ProxyPin',\n  //       'CN': 'ProxyPin CA (${Platform.localHostname})'\n  //     },\n  //     keyUsage: [\n  //       KeyUsage.DIGITAL_SIGNATURE,\n  //       KeyUsage.KEY_CERT_SIGN,\n  //       KeyUsage.CRL_SIGN\n  //     ],\n  //     extKeyUsage: [\n  //       ExtendedKeyUsage.SERVER_AUTH\n  //     ]);\n\n  // var generatePkcs12 = Pkcs12Utils.generatePkcs12(readAsString, [crt], password: '123');\n  // await File('/Users/wanghongen/Downloads/server.p12').writeAsBytes(generatePkcs12);\n}\n\n/// 生成证书\nString generate(X509CertificateData caRoot, RSAPublicKey serverPubKey, RSAPrivateKey caPriKey) {\n//根据CA证书subject来动态生成目标服务器证书的issuer和subject\n  Map<String, String> x509Subject = {\n    'C': 'CN',\n    'ST': 'BJ',\n    'L': 'Beijing',\n    'O': 'Proxy',\n    'OU': 'ProxyPin',\n  };\n  x509Subject['CN'] = 'ProxyPin CA (wanghongen)';\n\n  var csrPem = X509Utils.generateSelfSignedCertificate(caRoot, serverPubKey, caPriKey, 365,\n      keyUsage: x509.ExtensionKeyUsage(x509.ExtensionKeyUsage.keyCertSign | x509.ExtensionKeyUsage.cRLSign),\n      extKeyUsage: [ExtendedKeyUsage.SERVER_AUTH],\n      basicConstraints: BasicConstraints(isCA: true),\n      sans: [x509Subject['CN']!],\n      serialNumber: Random().nextInt(1000000).toString(),\n      subject: x509Subject);\n  return csrPem;\n}\n"
  },
  {
    "path": "test/favorites_trim_test.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/websocket.dart';\nimport 'package:proxypin/storage/favorites.dart';\n\nWebSocketFrame _frame(int index, {bool fromClient = true, int payloadBytes = 1024}) {\n  final frame = WebSocketFrame(\n    fin: true,\n    opcode: 0x01,\n    mask: false,\n    payloadLength: payloadBytes,\n    maskingKey: 0,\n    payloadData: Uint8List.fromList(List.filled(payloadBytes, index % 255)),\n    time: DateTime.fromMillisecondsSinceEpoch(1710000000000 + index),\n  );\n  frame.isFromClient = fromClient;\n  return frame;\n}\n\nvoid main() {\n  test('trimFavoriteMessages caps websocket frame count', () {\n    final request = HttpRequest(HttpMethod.get, 'https://example.com/ws');\n    final response = HttpResponse(HttpStatus.ok);\n    final favorite = Favorite(request, response: response);\n\n    for (int i = 0; i < FavoriteStorage.maxWebSocketMessagesPerFavorite + 20; i++) {\n      request.messages.add(_frame(i, fromClient: true, payloadBytes: 256));\n    }\n\n    final changed = FavoriteStorage.trimFavoriteMessages(favorite);\n    expect(changed, isTrue);\n    expect(\n      favorite.websocketMessageCount <= FavoriteStorage.maxWebSocketMessagesPerFavorite,\n      isTrue,\n    );\n\n    // newest frame remains\n    final newestTime = request.messages.last.time.millisecondsSinceEpoch;\n    expect(newestTime, 1710000000000 + FavoriteStorage.maxWebSocketMessagesPerFavorite + 19);\n  });\n\n  test('trimFavoriteMessages caps websocket payload bytes and keeps newest', () {\n    final request = HttpRequest(HttpMethod.get, 'https://example.com/ws');\n    final response = HttpResponse(HttpStatus.ok);\n    final favorite = Favorite(request, response: response);\n\n    for (int i = 0; i < 120; i++) {\n      final frame = _frame(i, fromClient: i.isEven, payloadBytes: 3 * 1024);\n      if (i.isEven) {\n        request.messages.add(frame);\n      } else {\n        response.messages.add(frame);\n      }\n    }\n\n    final changed = FavoriteStorage.trimFavoriteMessages(favorite);\n    expect(changed, isTrue);\n\n    final totalBytes = request.messages.fold<int>(0, (sum, e) => sum + e.payloadData.length) +\n        response.messages.fold<int>(0, (sum, e) => sum + e.payloadData.length);\n    expect(totalBytes <= FavoriteStorage.maxWebSocketPayloadBytesPerFavorite, isTrue);\n\n    // newest timestamp should still exist after trimming\n    final all = [...request.messages, ...response.messages]..sort((a, b) => a.time.compareTo(b.time));\n    expect(all.last.time.millisecondsSinceEpoch, 1710000000000 + 119);\n  });\n}\n\n"
  },
  {
    "path": "test/http_test.dart",
    "content": "import 'dart:io';\n\nmain() async {\n  var contentType = ContentType.parse(\"application/json\");\n  print(contentType);\n  print(contentType.charset);\n  print(Uri.parse(\"https://www.v2ex.com\").scheme);\n  // await socketTest();\n  await webTest();\n}\n\nwebTest() async {\n\n  var httpClient = HttpClient();\n  httpClient.findProxy = (uri) => \"PROXY 127.0.0.1:7890\";\n  // httpClient.badCertificateCallback = (X509Certificate cert, String host, int port) => true;\n  var httpClientRequest = await httpClient.getUrl(Uri.parse(\"https://www.v2ex.com\"));\n  var response = await httpClientRequest.close();\n  print(response.headers);\n}\n"
  },
  {
    "path": "test/js.js",
    "content": "async function onRequest() {\n\n\n    const fetchResponse = await fetch('https://httpbin.org/anything');\n    console.log(fetchResponse.headers);\n    console.log(await  fetchResponse.text());\n    console.log(  fetchResponse.body);\n\n    const response = {\n        statusCode: 200,\n        body: fetchResponse.body,\n        headers: fetchResponse.headers\n    };\n    return response;\n}\n\nonRequest().then( response => {\n    console.log('Response:', response);\n\n})"
  },
  {
    "path": "test/js_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_js/flutter_js.dart';\nimport 'package:proxypin/network/components/js/file.dart';\nimport 'package:proxypin/network/components/js/md5.dart';\n\n// Convert JS request\n// Map<String, dynamic> convertJsRequest(HttpRequest request) {\n//   return {\n//     'url': request.requestUrl,\n//     'path': request.path(),\n//     'headers': request.headers.toMap(),\n//     'method': request.method.name,\n//     'body': request.bodyAsString\n//   };\n// }\n\nmain() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  var flutterJs = getJavascriptRuntime();\n  Md5Bridge.registerMd5(flutterJs);\n  FileBridge.registerFile(flutterJs);\n\n  // var httpRequest = HttpRequest(HttpMethod.get, \"https://www.v2ex.com\");\n  // httpRequest.headers.set('user-agent', 'Dart/3.0 (dart:io)');\n\n  const code = \"\"\"\n    var context ='test';\n    var d = md5('value');\n    console.log(d);\n    var file = File('/Users/wanghongen/Downloads/test.html');\n    console.log(file.path);\n    // console.log(file.readAsStringSync());\n   \n    async function onRequest() {\n       await file.writeAsString('await');\n    \n       var text = await file.readAsString();\n       console.log(text);\n       File('/Users/wanghongen/Downloads/test.txt').create();\n    }\n    onRequest();\n  \"\"\";\n\n  // var jsRequest = jsonEncode(convertJsRequest(httpRequest));\n\n  var evaluate = await flutterJs.evaluateAsync(code);\n  // print(evaluate.stringResult);\n  await flutterJs.handlePromise(evaluate);\n  flutterJs.dartContext.clear();\n  flutterJs.localContext.clear();\n  flutterJs.evaluate('console.log(context)');\n}\n"
  },
  {
    "path": "test/pk12_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:proxypin/network/util/cert/pkcs12.dart';\n\nvoid main() {\n  const testPath = r\"C:\\Users\\wanghongen\\Downloads\\new_key.p12\";\n  if (!File(testPath).existsSync()) {\n    print('pk12_test local file missing - skipped');\n    return;\n  }\n\n  File file = File('C:\\\\Users\\\\wanghongen\\\\Downloads\\\\new_key.p12');\n  parsePKCS12([file], '01');\n\n  List<File> files = [];\n  files.add(File('C:\\\\Users\\\\wanghongen\\\\Downloads\\\\ProxyPinPkcs12.p12'));\n  files.add(File('C:\\\\Users\\\\wanghongen\\\\Downloads\\\\proxyman.p12'));\n  // files.add(File('C:\\\\Users\\\\wanghongen\\\\Downloads\\\\charles.p12'));\n  parsePKCS12(files, '123');\n}\n\nvoid parsePKCS12(List<File> files, String password) {\n  for (var file in files) {\n    var bytes = file.readAsBytesSync();\n    var decodePkcs12 = Pkcs12.parsePkcs12(bytes, password: password);\n\n    print(decodePkcs12[0]);\n    print(decodePkcs12[1]);\n  }\n}\n"
  },
  {
    "path": "test/requests_test.py",
    "content": "import requests\n\nurl = \"http://example.com/api\"\ncookies = {\n    \"session_id\": \"123456\",\n    \"user_name\": \"john\\\\doe\",\n}\n\nheaders = {\n    \"Host\": \"example.com\"\n}\n\nres = requests.get(url, headers=headers, cookies=cookies)\nprint(res.text)\n"
  },
  {
    "path": "test/temp_ipv6_test.dart",
    "content": "\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test('Parsed IPv6 authority correctly', () {\n    var authority = \"[240c:409f:1000::4:0:a5]\";\n    var scheme = \"https\";\n\n    String host = authority;\n    int port = (scheme == 'https' ? 443 : 80);\n\n    if (authority.startsWith(\"[\")) {\n      int closeBracketIndex = authority.indexOf(']');\n      if (closeBracketIndex != -1) {\n        host = authority.substring(0, closeBracketIndex + 1);\n        if (authority.length > closeBracketIndex + 1 && authority[closeBracketIndex + 1] == ':') {\n          port = int.tryParse(authority.substring(closeBracketIndex + 2)) ?? port;\n        }\n      }\n    } else {\n      int lastColonIndex = authority.lastIndexOf(':');\n      if (lastColonIndex != -1) {\n        var p = int.tryParse(authority.substring(lastColonIndex + 1));\n        if (p != null) {\n          host = authority.substring(0, lastColonIndex);\n          port = p;\n        }\n      }\n    }\n\n    expect(host, \"[240c:409f:1000::4:0:a5]\");\n    expect(port, 443);\n  });\n\n  test('Parsed IPv6 authority with port correctly', () {\n    var authority = \"[240c:409f:1000::4:0:a5]:8080\";\n    var scheme = \"https\";\n\n    String host = authority;\n    int port = (scheme == 'https' ? 443 : 80);\n\n    if (authority.startsWith(\"[\")) {\n      int closeBracketIndex = authority.indexOf(']');\n      if (closeBracketIndex != -1) {\n        host = authority.substring(0, closeBracketIndex + 1);\n        if (authority.length > closeBracketIndex + 1 && authority[closeBracketIndex + 1] == ':') {\n          port = int.tryParse(authority.substring(closeBracketIndex + 2)) ?? port;\n        }\n      }\n    } else {\n      int lastColonIndex = authority.lastIndexOf(':');\n      if (lastColonIndex != -1) {\n        var p = int.tryParse(authority.substring(lastColonIndex + 1));\n        if (p != null) {\n          host = authority.substring(0, lastColonIndex);\n          port = p;\n        }\n      }\n    }\n\n    expect(host, \"[240c:409f:1000::4:0:a5]\");\n    expect(port, 8080);\n  });\n\n  test('Parsed IPv6 authority broken', () {\n    // case from log: [240c\n    // Though log says host=[240c, authority was [240c:409f:1000::4:0:a5]\n    // This testcase is checking if the logic handles incomplete ipv6 gracefully?\n    // If authority is literally \"[240c\", it has start [ but no ].\n    var authority = \"[240c\";\n    var scheme = \"https\";\n\n    String host = authority;\n    int port = (scheme == 'https' ? 443 : 80);\n\n    if (authority.startsWith(\"[\")) {\n      int closeBracketIndex = authority.indexOf(']');\n      if (closeBracketIndex != -1) {\n        host = authority.substring(0, closeBracketIndex + 1);\n        if (authority.length > closeBracketIndex + 1 && authority[closeBracketIndex + 1] == ':') {\n          port = int.tryParse(authority.substring(closeBracketIndex + 2)) ?? port;\n        }\n      }\n    } else {\n       // ...\n    }\n    // Logic says: if start with [ but no ], host remains authority.\n    expect(host, \"[240c\");\n  });\n\n  test('Parsed IPv4 authority correctly', () {\n      var authority = \"192.168.1.1:8080\";\n      var scheme = \"http\";\n\n      String host = authority;\n      int port = (scheme == 'https' ? 443 : 80);\n\n      if (authority.startsWith(\"[\")) {\n          // ...\n      } else {\n        int lastColonIndex = authority.lastIndexOf(':');\n        if (lastColonIndex != -1) {\n          var p = int.tryParse(authority.substring(lastColonIndex + 1));\n          if (p != null) {\n            host = authority.substring(0, lastColonIndex);\n            port = p;\n          }\n        }\n      }\n      expect(host, \"192.168.1.1\");\n      expect(port, 8080);\n  });\n\n  test('Parsed simple hostname correctly', () {\n      var authority = \"example.com\";\n      var scheme = \"https\";\n\n      String host = authority;\n      int port = (scheme == 'https' ? 443 : 80);\n\n      if (authority.startsWith(\"[\")) {\n          // ...\n      } else {\n        int lastColonIndex = authority.lastIndexOf(':');\n        if (lastColonIndex != -1) {\n          var p = int.tryParse(authority.substring(lastColonIndex + 1));\n          if (p != null) {\n            host = authority.substring(0, lastColonIndex);\n            port = p;\n          }\n        }\n      }\n      expect(host, \"example.com\");\n      expect(port, 443);\n  });\n}\n\n"
  },
  {
    "path": "test/tests.dart",
    "content": "import 'dart:io';\n\nvoid main() async {\n  Uri.parse(\"https://[2408:8726:a000:f0:70::21]/\");\n  String str = 'https://zhihu.com/giftList?(?:[^&]*&)*page=1(?:&[^&]*)*\\$'.replaceAll(\"*\", \".*\")..replaceAll('?', '\\\\?');\n  print(RegExp(str).hasMatch(\"https://zhihu.com/giftList?type=1&sort=0&page=1&orderBy=desc&pageSize=20\"));\n  // print(RegExp('^www.baidu.com').hasMatch(\"https://www.baidu.com/wqeqweqe\"));\n  // String text = \"http://dddd/hello/world?name=dad&val=12a\";\n  // print(\"mame=\\$1123\".replaceAll(RegExp('\\\\\\$\\\\d'), \"123\"));\n  // print(\"app: ddd\".split(\": \"));\n  // print(text.replaceAllMapped(RegExp(\"name=(dad)\"), (match) {\n  //   var replaceAll = \"mame=\\$1-123\".replaceAll(\"\\$1\", match.group(1)!);\n  //\n  //   print(replaceAll);\n  //   return replaceAll;\n  // }));\n  // print(Platform.version);\n  print('localHostname: ${Platform.localHostname}');\n  // print(Platform.operatingSystem);\n  // print(Platform.localeName);\n  // print(Platform.script);\n}\n"
  },
  {
    "path": "test/url_test.dart",
    "content": "import 'package:proxypin/network/util/uri.dart';\n\nvoid main() {\n  String url =\n      \"https://edith.xiaohongshu.com/api/sns/v6/homefeed?client_volume=0.25&geo=eyJsYXRpdHVkZSI6NDAuMDU5MDkxOTAyNzk4OSwibG9uZ2l0dWRlIjoxMTYuNDEwMjA2OTMzNDYyN30%3D&known_signal=%7B%22nqe_level%22%3A6%2C%22hp_con%22%3A0%2C%22device_level%22%3A1%2C%22m_active%22%3A0%2C%22device_model%22%3A%22iPhone%2013%20Pro%22%2C%22hp_type%22%3A0%2C%22g_speed_y%22%3A1487.474609375%7D&last_card_position=4&last_live_id=&last_live_position=-1&loaded_ad=%7B%22loaded_ad_pos_list%22%3A%5B%5D%2C%22loaded_ad_real_pos_list%22%3A%5B%5D%2C%22ads_id_list%22%3A%5B%22663966499%22%2C%22243351939%22%2C%22230561227%22%5D%7D&num=20&oid=homefeed_recommend&orientation=portait&personalization=1&refresh_type=1&unread_begin_note_id=67136eaa0000000026034b5b&unread_end_note_id=67307eaf000000003c019503&unread_note_count=6&use_jpeg=1&user_action=0\";\n  print(url);\n  var uri = Uri.parse(url);\n  print(uri.queryParameters);\n\n  print(uri);\n  String query =\n      '{\"nqe_level\":6,\"hp_con\":0,\"device_level\":1,\"m_active\":0,\"device_model\":\"iPhone 13 Pro\",\"hp_type\":0,\"g_speed_y\":1487.474609375}';\n\n  print(Uri.encodeComponent(query));\n  // var splitQueryString = Uri.splitQueryString(uri.query);\n  print(UriUtils.mapToQuery(uri.queryParameters));\n  // print(uri.replace(queryParameters: splitQueryString));\n  print(uri.replace(query: UriUtils.mapToQuery(uri.queryParameters)));\n}\n"
  },
  {
    "path": "test/web_test.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:proxypin/network/channel/channel_context.dart';\nimport 'package:proxypin/network/http/codec.dart';\nimport 'package:proxypin/network/http/http.dart';\n\nmain() async {\n  await socketTest();\n}\n\nsocketTest() async {\n  var task = await Socket.startConnect(\"127.0.0.1\", 7890);\n  var socket = await task.socket;\n  if (socket.address.type != InternetAddressType.unix) {\n    socket.setOption(SocketOption.tcpNoDelay, true);\n  }\n\n  Completer<bool> completer = Completer<bool>();\n  StreamSubscription? subscription;\n  subscription = socket.listen((event) {\n    subscription!.pause();\n    print(String.fromCharCodes(event));\n    completer.complete(true);\n  });\n\n  String host = 'www.v2ex.com:443';\n\n  var httpRequest = HttpRequest(HttpMethod.connect, host);\n  httpRequest.headers.set('user-agent', 'Dart/3.0 (dart:io)');\n  httpRequest.headers.set('accept-encoding', 'gzip');\n  httpRequest.headers.set(HttpHeaders.hostHeader, host);\n  ChannelContext channelContext = ChannelContext();\n  var codec = HttpRequestCodec();\n  print(String.fromCharCodes(codec.encode(channelContext, httpRequest)));\n  socket.add(codec.encode(channelContext, httpRequest));\n  await socket.flush();\n\n  // subscription.resume();\n\n  await completer.future;\n  // await Future.delayed(const Duration(milliseconds: 1600));\n\n  var secureSocket = await SecureSocket.secure(socket, host: 'www.v2ex.com', onBadCertificate: (certificate) => true);\n  print(\"secureSocket\");\n  // await subscription.cancel();\n\n  completer = Completer<bool>();\n  subscription = secureSocket.listen((event) {\n    subscription?.pause();\n    print(String.fromCharCodes(event));\n    completer.complete(true);\n    subscription?.resume();\n  });\n\n  httpRequest = HttpRequest(HttpMethod.get, \"/\");\n  httpRequest.headers.set(HttpHeaders.hostHeader, host);\n\n  secureSocket.add(codec.encode(channelContext, httpRequest));\n  await secureSocket.flush();\n  await completer.future;\n}\n"
  },
  {
    "path": "test/websocket.dart",
    "content": "import 'dart:io';\n\nvoid main() async {\n// 连接到WebSocket服务器\n  var socket = await WebSocket.connect('wss://3hangzhou.goeasy.io/socket.io/?EIO=3&transport=websocket&b64=1');\n\n// 发送一个消息\n  socket.add('Hello, WebSocket Server!');\n\n// 监听接收的消息\n  socket.listen((data) {\n    print('Received: $data');\n  }, onError: (error) {\n    print('Error: $error');\n  }, onDone: () {\n    print('Connection closed');\n  });\n}\n"
  },
  {
    "path": "test/websocket_persistence_test.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:proxypin/network/http/http.dart';\nimport 'package:proxypin/network/http/websocket.dart';\n\nvoid main() {\n  test('HttpRequest persists websocket messages', () {\n    final request = HttpRequest(HttpMethod.get, 'https://example.com/ws');\n\n    final frame = WebSocketFrame(\n      fin: true,\n      opcode: 0x01,\n      mask: false,\n      payloadLength: 5,\n      maskingKey: 0,\n      payloadData: Uint8List.fromList('hello'.codeUnits),\n      time: DateTime.fromMillisecondsSinceEpoch(1710000000000),\n    )..isFromClient = true;\n\n    request.messages.add(frame);\n\n    final restored = HttpRequest.fromJson(request.toJson());\n    expect(restored.messages.length, 1);\n    expect(restored.messages.first.payloadDataAsString, 'hello');\n    expect(restored.messages.first.isFromClient, isTrue);\n    expect(restored.messages.first.time.millisecondsSinceEpoch, 1710000000000);\n  });\n\n  test('HttpResponse persists websocket messages', () {\n    final response = HttpResponse(HttpStatus.ok);\n\n    final frame = WebSocketFrame(\n      fin: true,\n      opcode: 0x02,\n      mask: false,\n      payloadLength: 3,\n      maskingKey: 0,\n      payloadData: Uint8List.fromList([1, 2, 3]),\n      time: DateTime.fromMillisecondsSinceEpoch(1710000001000),\n    )..isFromClient = false;\n\n    response.messages.add(frame);\n\n    final restored = HttpResponse.fromJson(response.toJson());\n    expect(restored.messages.length, 1);\n    expect(restored.messages.first.isBinary, isTrue);\n    expect(restored.messages.first.payloadData, [1, 2, 3]);\n    expect(restored.messages.first.time.millisecondsSinceEpoch, 1710000001000);\n  });\n\n"
  },
  {
    "path": "test/widget_test.dart",
    "content": "// This is a basic Flutter widget test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester\n// utility in the flutter_test package. For example, you can send tap and scroll\n// gestures. You can also use WidgetTester to find child widgets in the widget\n// tree, read text, and verify that the values of widget properties are correct.\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'package:proxypin/main.dart';\nimport 'package:proxypin/network/bin/configuration.dart';\nimport 'package:proxypin/ui/desktop/desktop.dart';\nimport 'package:proxypin/ui/configuration.dart';\n\nvoid main() {\n  testWidgets('Counter increments smoke test', (WidgetTester tester) async {\n    // Build our app and trigger a frame.\n    Configuration configuration = await Configuration.instance;\n    AppConfiguration appConfiguration = await AppConfiguration.instance;\n    await tester.pumpWidget(FluentApp(DesktopHomePage(configuration, appConfiguration), appConfiguration));\n\n    // Verify that our counter starts at 0.\n    expect(find.text('0'), findsOneWidget);\n    expect(find.text('1'), findsNothing);\n\n    // Tap the '+' icon and trigger a frame.\n    await tester.tap(find.byIcon(Icons.add));\n    await tester.pump();\n\n    // Verify that our counter has incremented.\n    expect(find.text('0'), findsNothing);\n    expect(find.text('1'), findsOneWidget);\n  });\n}\n"
  },
  {
    "path": "test/x509_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:proxypin/network/util/cert/x509.dart';\n\nvoid main() async {\n  // encoding();\n  // Add ext key usage 2.5.29.37\n// // Add key usage  2.5.29.15\n//   var keyUsage = [KeyUsage.KEY_CERT_SIGN, KeyUsage.CRL_SIGN];\n//\n//   var encode = keyUsageSequence(keyUsage)?.encode();\n//   print(Int8List.view(encode!.buffer));\n\n  var caPem = await File('assets/certs/ca.crt').readAsString();\n\n  // var caPem = File('/Users/wanghongen/Downloads/proxyman.crt').readAsStringSync();\n  //生成 公钥和私钥\n  var caRoot = X509Utils.x509CertificateFromPem(caPem);\n  var subject = caRoot.subject;\n  var d = X509Utils.getSubjectHashName(subject);\n\n  //16进制\n  print(d);\n  // var certPath = 'assets/certs/ca.crt';\n  //生成 公钥和私钥\n  // var caRoot = X509Utils.x509CertificateFromPem(caPem);\n  // print(caRoot.tbsCertificate.);\n  // caRoot.subject = X509Utils.getSubject(caRoot.subject);\n}\n\n//获取证书 subject hash\n\n// class KeyUsage {\n//   static const int keyCertSign = (1 << 2);\n//   static const int cRLSign = (1 << 1);\n//\n//   final ASN1BitString bitString;\n//\n//   KeyUsage(int usage) : bitString = ASN1BitString(stringValues: getBytes(usage))..unusedbits = getPadBits(usage);\n//\n//   static Uint8List getBytes(int bitString) {\n//     if (bitString == 0) {\n//       return Uint8List(0);\n//     }\n//\n//     int bytes = 4;\n//     for (int i = 3; i >= 1; i--) {\n//       if ((bitString & (0xFF << (i * 8))) != 0) {\n//         break;\n//       }\n//       bytes--;\n//     }\n//\n//     Uint8List result = Uint8List(bytes);\n//     for (int i = 0; i < bytes; i++) {\n//       result[i] = ((bitString >> (i * 8)) & 0xFF);\n//     }\n//\n//     return result;\n//   }\n//\n//   static int getPadBits(int bitString) {\n//     int val = 0;\n//     for (int i = 3; i >= 0; i--) {\n//       if (i != 0) {\n//         if ((bitString >> (i * 8)) != 0) {\n//           val = (bitString >> (i * 8)) & 0xFF;\n//           break;\n//         }\n//       } else {\n//         if (bitString != 0) {\n//           val = bitString & 0xFF;\n//           break;\n//         }\n//       }\n//     }\n//\n//     if (val == 0) {\n//       return 0;\n//     }\n//\n//     int bits = 1;\n//     while (((val <<= 1) & 0xFF) != 0) {\n//       bits++;\n//     }\n//\n//     return 8 - bits;\n//   }\n// }\n"
  },
  {
    "path": "windows/.gitignore",
    "content": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n"
  },
  {
    "path": "windows/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.14)\nproject(network LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"ProxyPin\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(SET CMP0063 NEW)\n\n# Define build configuration option.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n# Define settings for the Profile build mode.\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build; see runner/CMakeLists.txt.\nadd_subdirectory(\"runner\")\n\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# Support files are copied into place next to the executable, so that it can\n# run in place. This is done instead of making a separate bundle (as on Linux)\n# so that building and running from within Visual Studio will work.\nset(BUILD_BUNDLE_DIR \"$<TARGET_FILE_DIR:${BINARY_NAME}>\")\n# Make the \"install\" step default, as it's required to run.\nset(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\ninstall(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  CONFIGURATIONS Profile;Release\n  COMPONENT Runtime)\n"
  },
  {
    "path": "windows/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.14)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# Set fallback configurations for older versions of the flutter tool.\nif (NOT DEFINED FLUTTER_TARGET_PLATFORM)\n  set(FLUTTER_TARGET_PLATFORM \"windows-x64\")\nendif()\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      ${FLUTTER_TARGET_PLATFORM} $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.cc",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <desktop_multi_window/desktop_multi_window_plugin.h>\n#include <flutter_desktop_context_menu/flutter_desktop_context_menu_plugin.h>\n#include <flutter_js/flutter_js_plugin.h>\n#include <permission_handler_windows/permission_handler_windows_plugin.h>\n#include <proxy_manager/proxy_manager_plugin.h>\n#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>\n#include <share_plus/share_plus_windows_plugin_c_api.h>\n#include <url_launcher_windows/url_launcher_windows.h>\n#include <vclibs/vclibs_plugin_c_api.h>\n#include <win32audio/win32audio_plugin_c_api.h>\n#include <window_manager/window_manager_plugin.h>\n#include <zstandard_windows/zstandard_windows_plugin_c_api.h>\n\nvoid RegisterPlugins(flutter::PluginRegistry* registry) {\n  DesktopMultiWindowPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"DesktopMultiWindowPlugin\"));\n  FlutterDesktopContextMenuPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"FlutterDesktopContextMenuPlugin\"));\n  FlutterJsPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"FlutterJsPlugin\"));\n  PermissionHandlerWindowsPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"PermissionHandlerWindowsPlugin\"));\n  ProxyManagerPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"ProxyManagerPlugin\"));\n  ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"ScreenRetrieverWindowsPluginCApi\"));\n  SharePlusWindowsPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"SharePlusWindowsPluginCApi\"));\n  UrlLauncherWindowsRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"UrlLauncherWindows\"));\n  VclibsPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"VclibsPluginCApi\"));\n  Win32audioPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"Win32audioPluginCApi\"));\n  WindowManagerPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"WindowManagerPlugin\"));\n  ZstandardWindowsPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"ZstandardWindowsPluginCApi\"));\n}\n"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.h",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUGIN_REGISTRANT_\n\n#include <flutter/plugin_registry.h>\n\n// Registers Flutter plugins.\nvoid RegisterPlugins(flutter::PluginRegistry* registry);\n\n#endif  // GENERATED_PLUGIN_REGISTRANT_\n"
  },
  {
    "path": "windows/flutter/generated_plugins.cmake",
    "content": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  desktop_multi_window\n  flutter_desktop_context_menu\n  flutter_js\n  permission_handler_windows\n  proxy_manager\n  screen_retriever_windows\n  share_plus\n  url_launcher_windows\n  vclibs\n  win32audio\n  window_manager\n  zstandard_windows\n)\n\nlist(APPEND FLUTTER_FFI_PLUGIN_LIST\n)\n\nset(PLUGIN_BUNDLED_LIBRARIES)\n\nforeach(plugin ${FLUTTER_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})\n  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})\nendforeach(plugin)\n\nforeach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})\nendforeach(ffi_plugin)\n"
  },
  {
    "path": "windows/packaging/exe/inno_setup.sas",
    "content": "[Setup]\nAppId={{APP_ID}}\nAppVersion={{APP_VERSION}}\nAppName={{DISPLAY_NAME}}\nAppPublisher={{PUBLISHER}}\nAppPublisherURL={{PUBLISHER_URL}}\nAppSupportURL={{PUBLISHER_URL}}\nAppUpdatesURL={{PUBLISHER_URL}}\nDefaultDirName={{INSTALL_DIR_NAME}}\nDisableProgramGroupPage=yes\nOutputDir=.\nOutputBaseFilename={{OUTPUT_BASE_FILENAME}}\nCompression=lzma\nSolidCompression=yes\nSetupIconFile={{SETUP_ICON_FILE}}\nWizardStyle=modern\nCloseApplications=force\n\n[Languages]\n{% for locale in LOCALES %}\n{% if locale == 'en' %}Name: \"english\"; MessagesFile: \"compiler:Default.isl\"{% endif %}\n{% if locale == 'zh' %}Name: \"chinesesimplified\"; MessagesFile: \"compiler:Languages\\\\ChineseSimplified.isl\"{% endif %}\n{% if locale == 'ja' %}Name: \"japanese\"; MessagesFile: \"compiler:Languages\\\\Japanese.isl\"{% endif %}\n{% endfor %}\n\n[Tasks]\nName: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkablealone{% endif %}\nName: \"quicklaunchicon\"; Description: \"{cm:CreateQuickLaunchIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"; Flags: unchecked\n\n[Files]\nSource: \"{{SOURCE_DIR}}\\\\*\"; DestDir: \"{app}\"; Flags: ignoreversion recursesubdirs createallsubdirs\n; NOTE: Don't use \"Flags: ignoreversion\" on any shared system files\n\n[Icons]\nName: \"{autoprograms}\\\\{{DISPLAY_NAME}}\"; Filename: \"{app}\\\\{{EXECUTABLE_NAME}}\"\nName: \"{autodesktop}\\\\{{DISPLAY_NAME}}\"; Filename: \"{app}\\\\{{EXECUTABLE_NAME}}\"; Tasks: desktopicon\n\n[Run]\nFilename: \"{app}\\\\{{EXECUTABLE_NAME}}\"; Description: \"{cm:LaunchProgram,{{DISPLAY_NAME}}}\"; Flags: nowait postinstall skipifsilent\n"
  },
  {
    "path": "windows/packaging/exe/make_config.yaml",
    "content": "app_id: 502cbca5-a7f1-4f8f-894d-9820bac2e36f\npublisher: ProxyPin\npublisher_url: https://github.com/wanghongenpin/proxypin\ndisplay_name: ProxyPin\ncreate_desktop_icon: true\ninstall_dir_name: \"{autopf64}\\\\ProxyPin\"\nsetup_icon_file: windows\\runner\\resources\\app_icon.ico\nlocales:\n  - en\n  - zh\nscript_template: inno_setup.sas\n"
  },
  {
    "path": "windows/packaging/msix/make_config.yaml",
    "content": "display_name: ProxyPin\npublisher_display_name: ProxyPin\npublisher: CN=8EC6F6C3-E66C-4189-8421-A6F2A451F552\nidentity_name: ProxyPin.ProxyPin\npublisher_url: https://github.com/wanghongenpin/proxypin\nmsix_version: 1.2.6.0\nlogo_path: D:\\IdeaProjects\\proxypin\\assets\\icon.png\ncapabilities: internetClient\nstore: \"true\"\nlanguages: en-US,zh-CN"
  },
  {
    "path": "windows/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(runner LANGUAGES CXX)\n\n# Define the application target. To change its name, change BINARY_NAME in the\n# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer\n# work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add preprocessor definitions for the build version.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION=\\\"${FLUTTER_VERSION}\\\"\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}\")\n\n# Disable Windows macros that collide with C++ standard library functions.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\n\n# Add dependency libraries and include directories. Add any application-specific\n# dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_link_libraries(${BINARY_NAME} PRIVATE \"dwmapi.lib\")\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n"
  },
  {
    "path": "windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n#else\n#define VERSION_AS_NUMBER 1,0,0,0\n#endif\n\n#if defined(FLUTTER_VERSION)\n#define VERSION_AS_STRING FLUTTER_VERSION\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"com.proxy.pin\" \"\\0\"\n            VALUE \"FileDescription\", \"ProxyPin\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"ProxyPin\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2023 Hongen Wang. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"ProxyPin.exe\" \"\\0\"\n            VALUE \"ProductName\", \"ProxyPin\" \"\\0\"\n            VALUE \"ProductVersion\", VERSION_AS_STRING \"\\0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1252\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "windows/runner/flutter_window.cpp",
    "content": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n\n  flutter_controller_->engine()->SetNextFrameCallback([&]() {\n    this->Show();\n  });\n\n  // Flutter can complete the first frame before the \"show window\" callback is\n  // registered. The following call ensures a frame is pending to ensure the\n  // window is shown. It is a no-op if the first frame hasn't completed yet.\n  flutter_controller_->ForceRedraw();\n\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nLRESULT\nFlutterWindow::MessageHandler(HWND hwnd, UINT const message,\n                              WPARAM const wparam,\n                              LPARAM const lparam) noexcept {\n  // Give Flutter, including plugins, an opportunity to handle window messages.\n  if (flutter_controller_) {\n    std::optional<LRESULT> result =\n        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,\n                                                      lparam);\n    if (result) {\n      return *result;\n    }\n  }\n\n  switch (message) {\n    case WM_FONTCHANGE:\n      flutter_controller_->engine()->ReloadSystemFonts();\n      break;\n  }\n\n  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);\n}\n"
  },
  {
    "path": "windows/runner/flutter_window.h",
    "content": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n\n#include <memory>\n\n#include \"win32_window.h\"\n\n// A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n public:\n  // Creates a new FlutterWindow hosting a Flutter view running |project|.\n  explicit FlutterWindow(const flutter::DartProject& project);\n  virtual ~FlutterWindow();\n\n protected:\n  // Win32Window:\n  bool OnCreate() override;\n  void OnDestroy() override;\n  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,\n                         LPARAM const lparam) noexcept override;\n\n private:\n  // The project to run.\n  flutter::DartProject project_;\n\n  // The Flutter instance hosted by this window.\n  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;\n};\n\n#endif  // RUNNER_FLUTTER_WINDOW_H_\n"
  },
  {
    "path": "windows/runner/main.cpp",
    "content": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_window.h\"\n#include \"utils.h\"\n\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command) {\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  std::vector<std::string> command_line_arguments =\n      GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(980, 680);\n  if (!window.Create(L\"ProxyPin\", origin, size)) {\n    return EXIT_FAILURE;\n  }\n  window.SetQuitOnClose(true);\n\n  ::MSG msg;\n  while (::GetMessage(&msg, nullptr, 0, 0)) {\n    ::TranslateMessage(&msg);\n    ::DispatchMessage(&msg);\n  }\n\n  ::CoUninitialize();\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "windows/runner/resource.h",
    "content": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON                    101\n\n// Next default values for new objects\n//\n#ifdef APSTUDIO_INVOKED\n#ifndef APSTUDIO_READONLY_SYMBOLS\n#define _APS_NEXT_RESOURCE_VALUE        102\n#define _APS_NEXT_COMMAND_VALUE         40001\n#define _APS_NEXT_CONTROL_VALUE         1001\n#define _APS_NEXT_SYMED_VALUE           101\n#endif\n#endif\n"
  },
  {
    "path": "windows/runner/runner.exe.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10 and Windows 11 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"/>\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"/>\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"/>\n    </application>\n  </compatibility>\n</assembly>\n"
  },
  {
    "path": "windows/runner/utils.cpp",
    "content": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iostream>\n\nvoid CreateAndAttachConsole() {\n  if (::AllocConsole()) {\n    FILE *unused;\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stdout)) {\n      _dup2(_fileno(stdout), 1);\n    }\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stderr)) {\n      _dup2(_fileno(stdout), 2);\n    }\n    std::ios::sync_with_stdio();\n    FlutterDesktopResyncOutputStreams();\n  }\n}\n\nstd::vector<std::string> GetCommandLineArguments() {\n  // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.\n  int argc;\n  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);\n  if (argv == nullptr) {\n    return std::vector<std::string>();\n  }\n\n  std::vector<std::string> command_line_arguments;\n\n  // Skip the first argument as it's the binary name.\n  for (int i = 1; i < argc; i++) {\n    command_line_arguments.push_back(Utf8FromUtf16(argv[i]));\n  }\n\n  ::LocalFree(argv);\n\n  return command_line_arguments;\n}\n\nstd::string Utf8FromUtf16(const wchar_t* utf16_string) {\n  if (utf16_string == nullptr) {\n    return std::string();\n  }\n  int target_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, nullptr, 0, nullptr, nullptr)\n    -1; // remove the trailing null character\n  int input_length = (int)wcslen(utf16_string);\n  std::string utf8_string;\n  if (target_length <= 0 || target_length > utf8_string.max_size()) {\n    return utf8_string;\n  }\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      input_length, utf8_string.data(), target_length, nullptr, nullptr);\n  if (converted_length == 0) {\n    return std::string();\n  }\n  return utf8_string;\n}\n"
  },
  {
    "path": "windows/runner/utils.h",
    "content": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the process, and redirects stdout and stderr to\n// it for both the runner and the Flutter library.\nvoid CreateAndAttachConsole();\n\n// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string\n// encoded in UTF-8. Returns an empty std::string on failure.\nstd::string Utf8FromUtf16(const wchar_t* utf16_string);\n\n// Gets the command line arguments passed in as a std::vector<std::string>,\n// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.\nstd::vector<std::string> GetCommandLineArguments();\n\n#endif  // RUNNER_UTILS_H_\n"
  },
  {
    "path": "windows/runner/win32_window.cpp",
    "content": "#include \"win32_window.h\"\n\n#include <dwmapi.h>\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\n/// Window attribute that enables dark mode window decorations.\n///\n/// Redefined in case the developer's machine has a Windows SDK older than\n/// version 10.0.22000.0.\n/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute\n#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE\n#define DWMWA_USE_IMMERSIVE_DARK_MODE 20\n#endif\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n/// Registry key for app theme preference.\n///\n/// A value of 0 indicates apps should use dark mode. A non-zero or missing\n/// value indicates apps should use light mode.\nconstexpr const wchar_t kGetPreferredBrightnessRegKey[] =\n  L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize\";\nconstexpr const wchar_t kGetPreferredBrightnessRegValue[] = L\"AppsUseLightTheme\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n  }\n  FreeLibrary(user32_module);\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registrar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::Create(const std::wstring& title,\n                         const Point& origin,\n                         const Size& size) {\n  Destroy();\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  UpdateTheme(window);\n\n  return OnCreate();\n}\n\nbool Win32Window::Show() {\n  return ShowWindow(window_handle_, SW_SHOWNORMAL);\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n\n    case WM_DWMCOLORIZATIONCOLORCHANGED:\n      UpdateTheme(hwnd);\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n\nvoid Win32Window::UpdateTheme(HWND const window) {\n  DWORD light_mode;\n  DWORD light_mode_size = sizeof(light_mode);\n  LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,\n                               kGetPreferredBrightnessRegValue,\n                               RRF_RT_REG_DWORD, nullptr, &light_mode,\n                               &light_mode_size);\n\n  if (result == ERROR_SUCCESS) {\n    BOOL enable_dark_mode = light_mode == 0;\n    DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,\n                          &enable_dark_mode, sizeof(enable_dark_mode));\n  }\n}\n"
  },
  {
    "path": "windows/runner/win32_window.h",
    "content": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <memory>\n#include <string>\n\n// A class abstraction for a high DPI-aware Win32 Window. Intended to be\n// inherited from by classes that wish to specialize with custom\n// rendering and input handling\nclass Win32Window {\n public:\n  struct Point {\n    unsigned int x;\n    unsigned int y;\n    Point(unsigned int x, unsigned int y) : x(x), y(y) {}\n  };\n\n  struct Size {\n    unsigned int width;\n    unsigned int height;\n    Size(unsigned int width, unsigned int height)\n        : width(width), height(height) {}\n  };\n\n  Win32Window();\n  virtual ~Win32Window();\n\n  // Creates a win32 window with |title| that is positioned and sized using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size this function will scale the inputted width and height as\n  // as appropriate for the default monitor. The window is invisible until\n  // |Show| is called. Returns true if the window was created successfully.\n  bool Create(const std::wstring& title, const Point& origin, const Size& size);\n\n  // Show the current window. Returns true if the window was successfully shown.\n  bool Show();\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  // Update the window frame's theme to match the system theme.\n  static void UpdateTheme(HWND const window);\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  }
]