[
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: ['https://cdn.lpkt.cn/donate']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n<!--\nThanks for your feedback：\n1、Please check if there is a similar issue first\n2、If it is a function or UI suggestion, please go to Discussion\n-->\n\n**Describe the bug**\n<!--A clear and concise description of what the bug is.-->\n\n\n**To Reproduce**\n<!--\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n-->\n\n\n**Desired Results**\n<!--A clear description of the result you expected.-->\n\n\n**Actual Results**\n<!--A clear description of the actual result.-->\n\n\n**Screenshots**\n<!--If applicable, add screenshots(both bug screen and logs\nscreen) to help explain your problem.-->\n\n\n**Device**\n<!--\n- OS: e.g. iOS16 or Android13\n- App Version: e.g. 1.0.302\n-->\n\n\n**Additional context**\n<!--Add any other context about the problem here.-->\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report_cn.md",
    "content": "---\nname: Bug 反馈\nabout: 帮助我们改进错误\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!--\n感谢您反馈问题，请先阅读以下内容：\n1、请先查看是否已存在相似 issue\n2、如果是功能、UI建议，请移步至 Discussion\n-->\n\n**描述BUG**\n<!--一个关于BUG的清晰描述。-->\n\n\n**复现步骤**\n<!--\n如何复现：\n1. 打开 '...'\n2. 点击 '....'\n3. 滚动到 '....'\n4. 看到错误\n-->\n\n\n**期望结果**\n<!--一个关于你期望的结果的清晰描述。-->\n\n\n**实际结果**\n<!--一个关于实际结果的清晰描述。-->\n\n\n**截图**\n<!--如果可以，请添加截图（包括错误界面和日志界面）来帮助解释你的问题。-->\n\n\n**设备**\n<!--\n- 系统： e.g. iOS16 or Android13\n- App版本： e.g. 1.0.302\n-->\n\n\n**更多信息**\n<!--在这里添加关于这个问题的更多信息。-->\n\n"
  },
  {
    "path": ".github/workflows/analysis.yml",
    "content": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\nname: flutter analysis\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 1\n          submodules: recursive\n      \n      - uses: subosito/flutter-action@v2\n        with:\n          channel: 'stable'\n\n      - name: Install dependencies\n        run: flutter pub get\n\n      # Consider passing '--fatal-infos' for slightly stricter analysis.\n      - name: Analyze project source\n        run: flutter analyze lib test\n\n      # Your project will need to have tests in test/ and a dependency on\n      # package:test for this step to succeed. Note that Flutter projects will\n      # want to change this to 'flutter test'.\n      - name: Run tests\n        run: flutter test\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Flutter Release\n\non:\n  workflow_dispatch:\n  push:\n    tags:\n      - \"v*\"\n\npermissions:\n  contents: write\n\nenv:\n  APP_NAME: ServerBox\n  RELEASE_TAG: ${{ github.ref_name }}\n\njobs:\n  releaseAndroid:\n    name: Release android\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          submodules: recursive\n      - name: Install Flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version: \"3.41.4\"\n      - uses: actions/setup-java@v4\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n      - name: Fetch secrets\n        run: |\n          curl -u ${{ secrets.BASIC_AUTH }} -o android/app/app.key ${{ secrets.URL_PREFIX }}app.key\n          curl -u ${{ secrets.BASIC_AUTH }} -o android/key.properties ${{ secrets.URL_PREFIX }}key.properties\n      - name: Build\n        run: dart run fl_build -p android\n      - name: Rename for fdroid\n        shell: bash\n        run: |\n          APK_DIR=\"build/app/outputs/flutter-apk\"\n          shopt -s nullglob\n\n          for arch in arm64 arm amd64; do\n            matches=(\"$APK_DIR\"/\"${APP_NAME}\"_*_\"${arch}\".apk)\n            if [ ${#matches[@]} -ne 1 ]; then\n              echo \"Error: expected 1 APK for ${arch}, found ${#matches[@]}\"\n              echo \"APK_DIR: $APK_DIR\"\n              ls -la \"$APK_DIR\" || true\n              exit 1\n            fi\n            mv \"${matches[0]}\" \"$APK_DIR/${APP_NAME}_${RELEASE_TAG}_${arch}.apk\"\n          done\n      - name: Create Release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_arm64.apk\n            build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_arm.apk\n            build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_amd64.apk\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  releaseLinux:\n    name: Release linux\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          submodules: recursive\n      - name: Install Flutter\n        uses: subosito/flutter-action@v2\n      - name: Install dependencies\n        run: |\n          sudo apt update\n          # Basic\n          sudo apt install -y clang cmake ninja-build pkg-config libgtk-3-dev mesa-utils libvulkan-dev desktop-file-utils wget\n          # App Specific\n          sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev libsecret-1-dev\n      - name: Build\n        run: |\n          dart run fl_build -p linux\n      - name: Rename for release\n        shell: bash\n        run: |\n          shopt -s nullglob\n          matches=(\"${APP_NAME}\"_*_amd64.AppImage)\n          if [ ${#matches[@]} -ne 1 ]; then\n            echo \"Error: expected 1 AppImage, found ${#matches[@]}\"\n            ls -la || true\n            exit 1\n          fi\n          mv \"${matches[0]}\" \"${APP_NAME}_${RELEASE_TAG}_amd64.AppImage\"\n      - name: Create Release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            ${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_amd64.AppImage\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  releaseWin:\n    name: Release windows\n    runs-on: windows-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          submodules: recursive\n      - name: Install Flutter\n        uses: subosito/flutter-action@v2\n      - name: Build\n        run: dart run fl_build -p windows\n      - name: Rename for release\n        shell: bash\n        run: |\n          shopt -s nullglob\n          matches=(\"${APP_NAME}\"_*_windows_amd64.zip)\n          if [ ${#matches[@]} -ne 1 ]; then\n            echo \"Error: expected 1 zip, found ${#matches[@]}\"\n            ls -la || true\n            exit 1\n          fi\n          mv \"${matches[0]}\" \"${APP_NAME}_${RELEASE_TAG}_windows_amd64.zip\"\n      - name: Create Release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            ${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_windows_amd64.zip\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  # releaseIOS:\n  #   name: Release iOS\n  #   runs-on: macos-latest\n  #   steps:\n  #     - name: Checkout\n  #       uses: actions/checkout@v6\n  #       with:\n  #         submodules: recursive\n  #     - name: Install Flutter\n  #       uses: subosito/flutter-action@v2\n  #     - name: Build\n  #       run: |\n  #         dart run fl_build -p ios -- --no-codesign\n  #         shopt -s nullglob\n  #         IPA_FILES=(build/ios/ipa/*.ipa)\n  #         if [ ${#IPA_FILES[@]} -ne 1 ]; then\n  #           echo \"Error: expected 1 IPA, found ${#IPA_FILES[@]}\"\n  #           ls -la build/ios/ipa || true\n  #           exit 1\n  #         fi\n  #         IPA_FILE=\"${IPA_FILES[0]}\"\n  #         echo \"Found IPA: $IPA_FILE\"\n  #         cp \"$IPA_FILE\" \"${APP_NAME}_${RELEASE_TAG}_ios.ipa\"\n  #     - name: Create Release\n  #       uses: softprops/action-gh-release@v2\n  #       with:\n  #         files: |\n  #           ${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_ios.ipa\n  #       env:\n  #         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  # releaseMacOS:\n  #   name: Release macOS\n  #   runs-on: macos-latest\n  #   steps:\n  #     - name: Checkout\n  #       uses: actions/checkout@v6\n  #       with:\n  #         submodules: recursive\n  #     - name: Install Flutter\n  #       uses: subosito/flutter-action@v2\n  #     - name: Build\n  #       run: |\n  #         dart run fl_build -p macos -- --no-codesign\n  #     - name: Package\n  #       run: |\n  #         RELEASE_DIR=\"$GITHUB_WORKSPACE/build/macos/Build/Products/Release\"\n  #         APP_DIR=\"$RELEASE_DIR/$APP_NAME.app\"\n  #         OUT_ZIP=\"$GITHUB_WORKSPACE/${APP_NAME}_${RELEASE_TAG}_macos.zip\"\n\n  #         if [ ! -d \"$RELEASE_DIR\" ]; then\n  #           echo \"Error: macOS release directory not found: $RELEASE_DIR\"\n  #           exit 1\n  #         fi\n  #         if [ ! -d \"$APP_DIR\" ]; then\n  #           echo \"Error: macOS app bundle not found: $APP_DIR\"\n  #           exit 1\n  #         fi\n\n  #         cd \"$RELEASE_DIR\"\n  #         zip -ry \"$OUT_ZIP\" \"$APP_NAME.app\"\n  #     - name: Create Release\n  #       uses: softprops/action-gh-release@v2\n  #       with:\n  #         files: |\n  #           ${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_macos.zip\n  #       env:\n  #         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\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# Web related\nlib/generated_plugin_registrant.dart\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\n/android/app/fjy.androidstudio.key\n/android/app/app.key\n/release\ntest.dart\n\n# Keep generated l10n files\n# /.dart_tool/*\n# !/.dart_tool/flutter_gen\n\n.dart_tool\n\n# Linux release\nlinux.AppDir\n**/*.AppImage\n\nuntranlated.json\n\n.vscode/settings.json\nmore_build_data.json\ntrans.txt\nandroid/app/.cxx\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"dartssh2\"]\n\tpath = packages/dartssh2\n\turl = https://github.com/lollipopkit/dartssh2\n\tbranch = master\n[submodule \"xterm\"]\n\tpath = packages/xterm\n\turl = https://github.com/lollipopkit/xterm.dart\n\tbranch = master\n[submodule \"fl_lib\"]\n\tpath = packages/fl_lib\n\turl = https://github.com/lollipopkit/fl_lib\n\tbranch = before-sqlite\n[submodule \"fl_build\"]\n\tpath = packages/fl_build\n\turl = https://github.com/lppcg/fl_build.git\n\tbranch = main\n[submodule \"server_box_monitor\"]\n\tpath = packages/server_box_monitor\n\turl = https://github.com/lollipopkit/server_box_monitor\n\tbranch = main\n[submodule \"circle_chart\"]\n\tpath = packages/circle_chart\n\turl = https://github.com/lollipopkit/circle_chart\n\tbranch = main\n[submodule \"plain_notification_token\"]\n\tpath = packages/plain_notification_token\n\turl = https://github.com/lollipopkit/plain_notification_token\n\tbranch = master\n[submodule \"watch_connectivity\"]\n\tpath = packages/watch_connectivity\n\turl = https://github.com/lollipopkit/watch_connectivity\n\tbranch = master\n"
  },
  {
    "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 and should not be manually edited.\n\nversion:\n  revision: \"761747bfc538b5af34aa0d3fac380f1bc331ec49\"\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: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n      base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n    - platform: android\n      create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n      base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n    - platform: ios\n      create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n      base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n    - platform: linux\n      create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n      base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n    - platform: macos\n      create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n      base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n    - platform: web\n      create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n      base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n    - platform: windows\n      create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\n      base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49\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": ".vscode/launch.json",
    "content": "{\n    // 使用 IntelliSense 了解相关属性。 \n    // 悬停以查看现有属性的描述。\n    // 欲了解更多信息，请访问: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"debug\",\n            \"request\": \"launch\",\n            \"type\": \"dart\",\n            \"env\": {\n                // Comment this line to use the default display\n                \"DISPLAY\": \":1\"\n            }\n            // \"args\": [\n            //     \"-v\"\n            // ]\n        },\n        {\n            \"name\": \"profile\",\n            \"request\": \"launch\",\n            \"type\": \"dart\",\n            \"flutterMode\": \"profile\",\n            \"args\": [\n                \"--cache-sksl\",\n                // \"--purge-persistent-cache\"\n            ]\n        }\n    ]\n}"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Commands\n\n### Development\n\n- `flutter run` - Run the app in development mode\n- `dart run fl_build -p PLATFORM` - Build the app for specific platform (see fl_build package)\n- `dart run build_runner build --delete-conflicting-outputs` - Generate code for models with annotations (json_serializable, freezed, hive, riverpod)\n  - Every time you change model files, run this command to regenerate code (Hive adapters, Riverpod providers, etc.)\n  - Generated files include: `*.g.dart`, `*.freezed.dart` files\n\n### Testing\n\n- `flutter test` - Run unit tests\n- `flutter test test/battery_test.dart` - Run specific test file\n\n## Architecture\n\nThis is a Flutter application for managing Linux servers with the following key architectural components:\n\n### Project Structure\n\n- `lib/core/` - Core utilities, extensions, and routing\n- `lib/data/` - Data layer with models, providers, and storage\n  - `model/` - Data models organized by feature (server, container, ssh, etc.)\n  - `provider/` - Riverpod providers for state management\n  - `store/` - Local storage implementations using Hive\n- `lib/view/` - UI layer with pages and widgets\n- `lib/generated/` - Generated localization files\n- `lib/hive/` - Hive adapters for local storage\n\n### Key Technologies\n\n- **State Management**: Riverpod with code generation (riverpod_annotation)\n- **Local Storage**: Hive for persistent data with generated adapters\n- **SSH/SFTP**: Custom dartssh2 fork for server connections\n- **Terminal**: Custom xterm.dart fork for SSH terminal interface\n- **Networking**: dio for HTTP requests\n- **Charts**: fl_chart for server status visualization\n- **Localization**: Flutter's built-in i18n with ARB files\n- **Code Generation**: Uses build_runner with json_serializable, freezed, hive_generator, riverpod_generator\n\n### Data Models\n\n- Server management models in `lib/data/model/server/`\n- Container/Docker models in `lib/data/model/container/`\n- SSH and SFTP models in respective directories\n- Most models use freezed for immutability and json_annotation for serialization\n\n### Features\n\n- Server status monitoring (CPU, memory, disk, network)\n- SSH terminal with virtual keyboard\n- SFTP file browser\n- Docker container management\n- Process and systemd service management\n- Server snippets and custom commands\n- Multi-language support (12+ languages)\n- Cross-platform support (iOS, Android, macOS, Linux, Windows)\n\n### State Management Pattern\n\n- Uses Riverpod providers for dependency injection and state management\n- Uses Freezed for immutable state models\n- Providers are organized by feature in `lib/data/provider/`\n- State is often persisted using Hive stores in `lib/data/store/`\n\n### Build System\n\n- Uses custom `fl_build` package for cross-platform building\n- `make.dart` script handles pre/post build tasks (metadata generation)\n- Supports building for multiple platforms with platform-specific configurations\n- Many dependencies are custom forks hosted on GitHub (dartssh2, xterm, fl_lib, etc.)\n\n### Important Notes\n\n- **Never run code formatting commands** - The codebase has specific formatting that should not be changed\n- **Always run code generation** after modifying models with annotations (freezed, json_serializable, hive, riverpod)\n- Generated files (`*.g.dart`, `*.freezed.dart`) should not be manually edited\n- AGAIN, NEVER run code formatting commands.\n- USE dependency injection via GetIt for services like Stores, Services and etc.\n- Generate all l10n files using `flutter gen-l10n` command after modifying ARB files.\n- USE `hive_ce` not `hive` package for Hive integration.\n  - Which no need to config `HiveField` and `HiveType` manually.\n- USE widgets and utilities from `fl_lib` package for common functionalities.\n  - Such as `CustomAppBar`, `context.showRoundDialog`, `Input`, `Btnx.cancelOk`, etc.\n  - You can use context7 MCP to search `lppcg fl_lib KEYWORD` to find relevant widgets and utilities.\n- USE `libL10n` and `l10n` for localization strings.\n  - `libL10n` is from `fl_lib` package, and `l10n` is from this project.\n  - Before adding new strings, check if it already exists in `libL10n`.\n  - Prioritize using strings from `libL10n` to avoid duplication, even if the meaning is not 100% exact, just use the substitution of `libL10n`.\n- Split UI into Widget build, Actions, Utils. use `extension on` to achieve this\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "README.md",
    "content": "English | [简体中文](README_zh.md)\n\n<h2 align=\"center\">Flutter Server Box</h2>\n\n<div align=\"center\">\n  <a href=\"https://cdn.lpkt.cn/donate\"><img alt=\"donate\" src=\"https://img.shields.io/badge/donate-me-pink\"></a>\n  <img alt=\"lang\" src=\"https://img.shields.io/badge/lang-dart-cyan\">\n  <img alt=\"license\" src=\"https://img.shields.io/badge/license-AGPLv3-yellow\">\n  <a href=\"https://deepwiki.com/lollipopkit/flutter_server_box\"><img src=\"https://deepwiki.com/badge.svg\" alt=\"Ask DeepWiki\"></a>\n</div>\n\n<p align=\"center\">\nA Flutter project which provides charts to display Linux, Unix and Windows server status and tools to manage servers.\n<br>\nEspecially thanks to <a href=\"https://github.com/TerminalStudio/dartssh2\">dartssh2</a> & <a href=\"https://github.com/TerminalStudio/xterm.dart\">xterm.dart</a>.\n</p>\n\n## 🏙️ Screenshots\n\n<table>\n  <tr>\n    <td><img width=\"200px\" src=\"https://cdn.lpkt.cn/serverbox/screenshot/1.jpg\"></td>\n    <td><img width=\"200px\" src=\"https://cdn.lpkt.cn/serverbox/screenshot/2.jpg\"></td>\n    <td><img width=\"200px\" src=\"https://cdn.lpkt.cn/serverbox/screenshot/3.jpg\"></td>\n    <td><img width=\"200px\" src=\"https://cdn.lpkt.cn/serverbox/screenshot/4.jpg\"></td>\n  </tr>\n</table>\n\n## 📥 Installation\n\n|Platform| From|\n|--|--|\n| iOS / macOS     | [AppStore](https://apps.apple.com/app/id1586449703)                                                                                                                                                                                                                               |\n| Android         | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) |\n| Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid)                                                                                                                                 |\n\nPlease only download pkgs from the source that **you trust**!  \n\n## 🔖 Features\n\n- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`, `S.M.A.R.T`...\n- Platform specific: `Bio auth`、`Msg push`、`Home widget`、`watchOS App`...\n- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); Español, Русский язык, Português, 日本語 (Generated by GPT)\n\n## 🆘 Help\n\n<div align=\"center\">\n  <a href=\"https://qm.qq.com/q/daCGa7eShG\"><img alt=\"qq\" src=\"https://img.shields.io/badge/QQ-Group-pink\"></a>\n  <a href=\"https://t.me/lpktg\"><img alt=\"donate\" src=\"https://img.shields.io/badge/Telegram-lpktg-green\"></a>\n  <a href=\"https://discord.gg/SsVNbRhK7w\"><img alt=\"discord\" src=\"https://img.shields.io/badge/Discord-lpkt-purple\"></a>\n</div>\n\n- In order to push  server status to your portable device without opening ServerBox app (Such as **message push** and **home widget**), you need to install [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor) on your servers, and config it correctly. See [wiki](https://github.com/lollipopkit/server_box_monitor/wiki) for more details.\n- **Common issues** can be found in [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki).\n\nBefore you open an issue, please read the following:\n\n1. Paste the **entire log** (click the top right of the home page) in the issue template.\n2. Make sure whether the issue is caused by ServerBox app.\n3. Welcome all valid and positive feedback, subjective feedback (such as you think other UI is better) may not be accepted.\n\nAfter you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).\n\n## 🧱 Contributions\n\nAny positive contribution is welcome.\n\nIf I forgot to add your name to the contributors list, please add a comment in the issue or PR you opened to let me know, I will add it as soon as possible.\n\n### Development\n\n1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment.\n2. Clone this repo, run `flutter run` to start the app.\n3. Run `dart run fl_build -p PLATFORM` to build the app.\n\n### Translation\n\n- [Guide](https://blog.lpkt.cn/posts/faq/) can be found in my blog.\n- We need your help! Just feel free to open a PR.\n\n## 💡 My other apps\n\n- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - A third-party GPT Client for OpenAI API on all platforms.\n- [More](https://github.com/lollipopkit) - Tools & etc.\n\n## 📝 License\n\n`AGPL v3 lollipopkit & all contributors`\n"
  },
  {
    "path": "README_zh.md",
    "content": "简体中文 | [English](README.md)\n\n<h2 align=\"center\">Flutter Server Box</h2>\n\n<div align=\"center\">\n  <a href=\"https://cdn.lpkt.cn/donate\"><img alt=\"donate\" src=\"https://img.shields.io/badge/捐赠-我-pink\"></a>\n  <img alt=\"语言\" src=\"https://img.shields.io/badge/语言-dart-cyan\">\n  <img alt=\"license\" src=\"https://img.shields.io/badge/证书-AGPLv3-yellow\">\n  <a href=\"https://deepwiki.com/lollipopkit/flutter_server_box\"><img src=\"https://deepwiki.com/badge.svg\" alt=\"Ask DeepWiki\"></a>\n</div>\n\n<p align=\"center\">\n使用 Flutter 开发的 Linux, Unix, Windows 服务器工具箱，提供服务器状态图表和管理工具。\n<br>\n特别感谢 <a href=\"https://github.com/TerminalStudio/dartssh2\">dartssh2</a> & <a href=\"https://github.com/TerminalStudio/xterm.dart\">xterm.dart</a>。\n</p>\n\n## 🏙️ 截屏\n\n<table>\n  <tr>\n    <td><img width=\"200px\" src=\"https://cdn.lpkt.cn/serverbox/screenshot/1.jpg\"></td>\n    <td><img width=\"200px\" src=\"https://cdn.lpkt.cn/serverbox/screenshot/2.jpg\"></td>\n    <td><img width=\"200px\" src=\"https://cdn.lpkt.cn/serverbox/screenshot/3.jpg\"></td>\n    <td><img width=\"200px\" src=\"https://cdn.lpkt.cn/serverbox/screenshot/4.jpg\"></td>\n  </tr>\n</table>\n\n## 📥 安装\n\n平台|下载\n--|--\niOS / macOS     | [AppStore](https://apps.apple.com/app/id1586449703)\nAndroid         | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)\nLinux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid)\n\n请从 **信任** 的来源下载!\n\n## 🔖 特点\n\n- `状态图表`（CPU、传感器、GPU 等）, `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理，`S.M.A.R.T`...\n- 特殊支持：`生物认证`、`推送`、`桌面小部件`、`watchOS App`、`跟随系统颜色`...\n- 本地化\n  - English, 简体中文\n  - Español, Русский язык, Português, 日本語 (Generated by GPT)\n  - Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix);\n  - 感谢贡献者们！\n\n## 🆘 帮助\n\n<div align=\"center\">\n  <a href=\"https://qm.qq.com/q/daCGa7eShG\"><img alt=\"qq\" src=\"https://img.shields.io/badge/QQ-群-pink\"></a>\n  <a href=\"https://t.me/lpktg\"><img alt=\"donate\" src=\"https://img.shields.io/badge/Telegram-lpktg-green\"></a>\n  <a href=\"https://discord.gg/SsVNbRhK7w\"><img alt=\"discord\" src=\"https://img.shields.io/badge/Discord-lpkt-purple\"></a>\n</div>\n\n- 为了可以在不使用 ServerBox app 时获取服务器状态（例如：桌面小部件、推送服务），你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor)，详情见 [wiki](https://github.com/lollipopkit/server_box_monitor/wiki/%E4%B8%BB%E9%A1%B5)。  \n- **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。\n\n反馈前须知：\n\n1. 反馈问题请附带 log（点击首页右上角），并以 bug 模版提交。\n2. 反馈问题前请检查是否是 serverbox 的问题。\n3. 欢迎所有有效、正面的反馈，主观（比如你觉得其他UI更好看）的反馈不一定会接受\n\n## 🧱 贡献\n\n任何正面的贡献都欢迎。\n\n如果我忘记在贡献者列表中添加你的名字，请在你打开的 issue 或 PR 中添加评论让我知道，我会尽快添加。\n\n### 开发\n\n1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)\n2. 克隆这个仓库, 运行 `flutter run` 启动应用\n3. 运行 `dart run fl_build -p PLATFORM` 构建应用\n\n### 翻译\n\n[指南](https://blog.lpkt.cn/faq/) 可在我的博客中找到。\n\n## 💡 我的其它 Apps\n\n- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。\n- [更多](https://github.com/lollipopkit) - 工具 & etc.\n\n## 📝 协议\n\n`AGPL v3 lollipopkit & 所有贡献者`\n"
  },
  {
    "path": "analysis_options.yaml",
    "content": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n# The issues identified by the analyzer are surfaced in the UI of Dart-enabled\n# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be\n# invoked from the command line by running `flutter analyze`.\n\n# 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\nanalyzer:\n  exclude:\n    - \"**/*.g.dart\"\n  language:\n    # strict-casts: true\n    # strict-inference: true\n    # strict-raw-types: true\n  errors:\n    invalid_annotation_target: ignore\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    library_private_types_in_public_api: true\n    use_build_context_synchronously: false\n    depend_on_referenced_packages: false\n    prefer_final_locals: true\n    unnecessary_parenthesis: true\n    implicit_call_tearoffs: true\n    always_declare_return_types: true\n    always_use_package_imports: true\n    annotate_overrides: true\n    avoid_empty_else: true\n    # avoid_print: false  # Uncomment to disable the `avoid_print` rule\n    prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule\n    avoid_return_types_on_setters: true\n    directives_ordering: true # Enable sorting of imports\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"
  },
  {
    "path": "android/app/build.gradle",
    "content": "plugins {\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 keystoreProperties = new Properties()\ndef keystorePropertiesFile = rootProject.file('key.properties')\nif (keystorePropertiesFile.exists()) {\n    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))\n} else {\n    System.err.printf(\" [!] key.properties not found in %s (%s). Build will fail. \\n\", rootProject, rootProject.file('.'))\n}\n\nif (keystoreProperties['storeFile'] == null || !file(keystoreProperties['storeFile']).exists()) {\n    System.err.printf(\" [!] storeFile defined in key.properties does not exist in %s. Build will fail. \\n\", file('.'))\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\nandroid {\n    namespace \"tech.lolli.toolbox\"\n    compileSdk flutter.compileSdkVersion\n    ndkVersion flutter.ndkVersion\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    defaultConfig {\n        applicationId \"tech.lolli.toolbox\"\n        // You can update the following values to match your application needs.\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        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n        \n        ndk {\n            if(!splits.abi.enable) { // abiFilters cannot be present when splits abi filters are set\n                abiFilters 'arm64-v8a', 'armeabi-v7a'\n            }\n        }\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            signingConfig signingConfigs.release\n            minifyEnabled true\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n\n        debug {\n            // No applicationIdSuffix or resValue here\n        }\n\n        profile {\n            // No applicationIdSuffix or resValue here\n        }\n    }\n\n    dependenciesInfo {\n        // Disables dependency metadata when building APKs.\n        includeInApk = false\n        // Disables dependency metadata when building Android App Bundles.\n        includeInBundle = false\n    }\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {}\n\next.abiCodes = [\"x86_64\": 1, \"armeabi-v7a\": 2, \"arm64-v8a\": 3]\nimport com.android.build.OutputFile\nandroid.applicationVariants.all { variant ->\n  variant.outputs.each { output ->\n    def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))\n    if (abiVersionCode != null) {\n      output.versionCodeOverride = variant.versionCode * 100 + abiVersionCode\n    }\n  }\n}\n"
  },
  {
    "path": "android/app/proguard-rules.pro",
    "content": "-keep class com.jcraft.**  { *; }\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.USE_BIOMETRIC\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_DATA_SYNC\" />\n    <uses-permission android:name=\"android.permission.VIBRATE\" />\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n\n    <application\n        android:label=\"@string/app_name\"\n        android:name=\"${applicationName}\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:allowBackup=\"true\" \n        android:fullBackupContent=\"@xml/backup_rules\"\n        android:hasFragileUserData=\"true\"\n        android:restoreAnyVersion=\"true\"\n        tools:targetApi=\"q\">\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/LaunchTheme\"\n            android:configChanges=\"keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|layoutDirection|fontScale|density|uiMode\"\n            android:hardwareAccelerated=\"true\"\n            android:windowSoftInputMode=\"adjustResize\">\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            />\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        <!-- 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\n        <activity\n            android:name=\".widget.WidgetConfigureActivity\"\n            android:exported=\"false\"\n            android:theme=\"@android:style/Theme.Material.Light.Dialog\">\n            <intent-filter>\n                <action android:name=\"android.appwidget.action.APPWIDGET_CONFIGURE\" />\n            </intent-filter>\n        </activity>\n\n        <receiver\n            android:name=\".widget.HomeWidget\"\n            android:exported=\"false\"\n            android:label=\"StatusWidget\">\n            <intent-filter>\n                <action android:name=\"android.appwidget.action.APPWIDGET_UPDATE\" />\n                <action android:name=\"tech.lolli.toolbox.UPDATE_WIDGET\" />\n                <action android:name=\"miui.appwidget.action.APPWIDGET_UPDATE\" />\n            </intent-filter>\n            <!--\n                 <meta-data\n                android:name=\"miuiWidget\"\n                android:value=\"true\" />\n            -->\n            <meta-data\n                android:name=\"android.appwidget.provider\"\n                android:resource=\"@xml/home_widget\" />\n        </receiver>\n\n        <service\n            android:name=\".ForegroundService\"\n            android:enabled=\"true\"\n            android:foregroundServiceType=\"dataSync\"\n            android:exported=\"false\" />\n\n    </application>\n    <!-- Required to query activities that can process text, see:\n         https://developer.android.com/training/package-visibility?hl=en and\n         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.\n\n         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.PROCESS_TEXT\" />\n            <data android:mimeType=\"text/plain\" />\n        </intent>\n    </queries>\n</manifest>"
  },
  {
    "path": "android/app/src/main/kotlin/tech/lolli/toolbox/ForegroundService.kt",
    "content": "package tech.lolli.toolbox\n\nimport android.app.*\nimport android.content.Intent\nimport android.content.pm.ServiceInfo\nimport android.graphics.drawable.Icon\nimport android.os.Build\nimport android.os.IBinder\nimport android.util.Log\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.io.File\nimport java.util.*\n\nclass ForegroundService : Service() {\n    companion object {\n        @Volatile\n        var isRunning: Boolean = false\n    }\n    private val chanId = \"ForegroundServiceChannel\"\n    private val NOTIFICATION_ID = 1000\n    private val ACTION_STOP_FOREGROUND = \"ACTION_STOP_FOREGROUND\"\n    private val ACTION_UPDATE_SESSIONS = \"tech.lolli.toolbox.ACTION_UPDATE_SESSIONS\"\n    private val ACTION_DISCONNECT_SESSION = \"tech.lolli.toolbox.ACTION_DISCONNECT_SESSION\"\n\n    private var isFgStarted = false\n    private val postedIds = mutableSetOf<Int>()\n    // Stable mapping from session-id -> notification-id to avoid hash collisions\n    private val notificationIdMap = mutableMapOf<String, Int>()\n    private val nextNotificationId = java.util.concurrent.atomic.AtomicInteger(2001)\n\n    private fun logError(message: String, error: Throwable? = null) {\n        Log.e(\"ForegroundService\", message, error)\n        try {\n            val logFile = File(getExternalFilesDir(null), \"server_box.log\")\n            val timestamp = java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\", Locale.US).format(Date())\n            val logMessage = \"$timestamp [ForegroundService] ERROR: $message\\n${error?.stackTraceToString() ?: \"\"}\\n\"\n            logFile.appendText(logMessage)\n        } catch (e: Exception) {\n            Log.e(\"ForegroundService\", \"Failed to write log\", e)\n        }\n    }\n\n    override fun onCreate() {\n        super.onCreate()\n        Log.d(\"ForegroundService\", \"Service onCreate\")\n        isRunning = true\n        createNotificationChannel()\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        try {\n            // Check notification permission for Android 13+\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&\n                androidx.core.content.ContextCompat.checkSelfPermission(\n                    this, android.Manifest.permission.POST_NOTIFICATIONS\n                ) != android.content.pm.PackageManager.PERMISSION_GRANTED\n            ) {\n                Log.w(\"ForegroundService\", \"Notification permission denied. Stopping service gracefully.\")\n                // Don't call stopForegroundService() here as we haven't started foreground yet\n                stopSelf()\n                return START_NOT_STICKY\n            }\n\n            if (intent == null) {\n                Log.w(\"ForegroundService\", \"onStartCommand called with null intent\")\n                // Don't call stopForegroundService() here as we haven't started foreground yet\n                stopSelf()\n                return START_NOT_STICKY\n            }\n\n            val action = intent.action\n            Log.d(\"ForegroundService\", \"onStartCommand action=$action\")\n\n            return when (action) {\n                ACTION_STOP_FOREGROUND -> {\n                    // Notify Flutter to stop all connections before stopping service\n                    val stopAllIntent = Intent(\"tech.lolli.toolbox.STOP_ALL_CONNECTIONS\")\n                    sendBroadcast(stopAllIntent)\n                    clearAll()\n                    stopForegroundService()\n                    START_NOT_STICKY\n                }\n                ACTION_UPDATE_SESSIONS -> {\n                    val payload = intent.getStringExtra(\"payload\") ?: \"{}\"\n                    handleUpdateSessions(payload)\n                    START_STICKY\n                }\n                else -> {\n                    // Default bring up foreground with placeholder\n                    ensureForeground(createMergedNotification(0, emptyList(), emptyList()))\n                    START_STICKY\n                }\n            }\n            \n        } catch (e: Exception) {\n            logError(\"Error in onStartCommand\", e)\n            stopSelf()\n            return START_NOT_STICKY\n        }\n    }\n\n    override fun onBind(intent: Intent?): IBinder? {\n        return null\n    }\n\n    private fun createNotificationChannel() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            try {\n                val manager = getSystemService(NotificationManager::class.java)\n                if (manager == null) {\n                    Log.e(\"ForegroundService\", \"Failed to get NotificationManager\")\n                    return\n                }\n                val serviceChannel = NotificationChannel(\n                    chanId,\n                    \"ForegroundServiceChannel\",\n                    NotificationManager.IMPORTANCE_DEFAULT\n                ).apply {\n                    description = \"For foreground service\"\n                }\n                manager.createNotificationChannel(serviceChannel)\n                Log.d(\"ForegroundService\", \"Notification channel created successfully\")\n            } catch (e: Exception) {\n                logError(\"Failed to create notification channel\", e)\n            }\n        }\n    }\n\n    private fun ensureForeground(notification: Notification) {\n        try {\n            // Double-check notification permission before starting foreground service\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&\n                androidx.core.content.ContextCompat.checkSelfPermission(\n                    this, android.Manifest.permission.POST_NOTIFICATIONS\n                ) != android.content.pm.PackageManager.PERMISSION_GRANTED\n            ) {\n                Log.w(\"ForegroundService\", \"Cannot start foreground service without notification permission\")\n                stopSelf()\n                return\n            }\n\n            if (!isFgStarted) {\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                    startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)\n                } else {\n                    startForeground(NOTIFICATION_ID, notification)\n                }\n                isFgStarted = true\n                Log.d(\"ForegroundService\", \"Foreground service started successfully\")\n            } else {\n                val nm = getSystemService(NotificationManager::class.java)\n                if (nm != null) {\n                    nm.notify(NOTIFICATION_ID, notification)\n                } else {\n                    Log.w(\"ForegroundService\", \"NotificationManager is null, cannot update notification\")\n                }\n            }\n        } catch (e: SecurityException) {\n            logError(\"Security exception when starting foreground service (likely missing permission)\", e)\n            stopSelf()\n        } catch (e: Exception) {\n            logError(\"Failed to start/update foreground\", e)\n            // Don't stop the service for other exceptions, just log them\n        }\n    }\n\n\n    private fun createMergedNotification(count: Int, lines: List<String>, sessions: List<SessionItem>): Notification {\n        val notificationIntent = Intent(this, MainActivity::class.java)\n        val pendingIntent = PendingIntent.getActivity(\n            this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE\n        )\n        val stopIntent = Intent(this, ForegroundService::class.java).apply { action = ACTION_STOP_FOREGROUND }\n        val stopPending = PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE)\n\n        val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            Notification.Builder(this, chanId)\n        } else {\n            @Suppress(\"DEPRECATION\")\n            Notification.Builder(this)\n        }\n\n        // Use the earliest session's start time for chronometer\n        val earliestStartTime = sessions.minOfOrNull { it.startWhen } ?: System.currentTimeMillis()\n\n        val title = when (count) {\n            0 -> \"Server Box\"\n            1 -> sessions.first().title\n            else -> \"SSH sessions: $count active\"\n        }\n\n        val contentText = when (count) {\n            0 -> \"Ready for connections\"\n            1 -> {\n                val session = sessions.first()\n                \"${session.subtitle} · ${session.status}\"\n            }\n            else -> \"Multiple SSH connections active\"\n        }\n\n        // For multiple sessions, show details in expanded view\n        val style = if (count > 1) {\n            val inbox = Notification.InboxStyle()\n            val maxLines = 5\n            val displayLines = if (lines.size > maxLines) {\n                lines.take(maxLines) + \"...and ${lines.size - maxLines} more\"\n            } else {\n                lines\n            }\n            displayLines.forEach { inbox.addLine(it) }\n            inbox.setBigContentTitle(title)\n            inbox\n        } else {\n            null\n        }\n\n        val notification = builder\n            .setContentTitle(title)\n            .setContentText(contentText)\n            .setSmallIcon(R.mipmap.ic_launcher)\n            .setWhen(earliestStartTime)\n            .setUsesChronometer(true)\n            .setOngoing(true)\n            .setOnlyAlertOnce(true)\n            .setContentIntent(pendingIntent)\n            .addAction(\n                Notification.Action.Builder(\n                    Icon.createWithResource(this, android.R.drawable.ic_delete),\n                    \"Stop All\",\n                    stopPending\n                ).build()\n            )\n\n        if (style != null) {\n            notification.setStyle(style)\n        }\n\n        return notification.build()\n    }\n\n    private fun handleUpdateSessions(payload: String) {\n        val nm = getSystemService(NotificationManager::class.java)\n        if (nm == null) {\n            logError(\"NotificationManager null\")\n            return\n        }\n\n        val sessions = mutableListOf<SessionItem>()\n        try {\n            val obj = JSONObject(payload)\n            val arr: JSONArray = obj.optJSONArray(\"sessions\") ?: JSONArray()\n            for (i in 0 until arr.length()) {\n                val s = arr.optJSONObject(i) ?: continue\n                val id = s.optString(\"id\")\n                val title = s.optString(\"title\")\n                val sub = s.optString(\"subtitle\")\n                val whenMs = s.optLong(\"startTimeMs\", System.currentTimeMillis())\n                val status = s.optString(\"status\", \"connected\")\n                if (id.isNotEmpty()) {\n                    sessions.add(SessionItem(id, title, sub, whenMs, status))\n                }\n            }\n        } catch (e: Exception) {\n            logError(\"Failed to parse payload\", e)\n        }\n\n        // Clear if empty\n        if (sessions.isEmpty()) {\n            clearAll()\n            return\n        }\n\n        // Cancel any existing individual notifications (we only show merged notification now)\n        val toCancel = postedIds.toSet()\n        toCancel.forEach { nm.cancel(it) }\n        postedIds.clear()\n        notificationIdMap.clear()\n\n        // Create merged notification content\n        val summaryLines = sessions.map { \"${it.title}: ${it.status}\" }\n        val mergedNotification = createMergedNotification(sessions.size, summaryLines, sessions)\n        ensureForeground(mergedNotification)\n    }\n\n    private fun clearAll() {\n        val nm = getSystemService(NotificationManager::class.java)\n        nm?.cancel(NOTIFICATION_ID)\n        postedIds.forEach { id -> nm?.cancel(id) }\n        postedIds.clear()\n        isFgStarted = false\n    }\n\n    data class SessionItem(\n        val id: String,\n        val title: String,\n        val subtitle: String,\n        val startWhen: Long,\n        val status: String,\n    )\n\n    private fun stopForegroundService() {\n        try {\n            if (isFgStarted) {\n                stopForeground(STOP_FOREGROUND_REMOVE)\n                isFgStarted = false\n            }\n        } catch (e: Exception) {\n            logError(\"Error stopping foreground\", e)\n        }\n        stopSelf()\n        Log.d(\"ForegroundService\", \"ForegroundService stopped\")\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        Log.d(\"ForegroundService\", \"Service onDestroy\")\n        isRunning = false\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/tech/lolli/toolbox/MainActivity.kt",
    "content": "package tech.lolli.toolbox\n\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.os.Build\nimport android.Manifest\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.IntentFilter\nimport androidx.core.app.ActivityCompat\nimport androidx.core.content.ContextCompat\nimport io.flutter.embedding.android.FlutterFragmentActivity\nimport io.flutter.embedding.engine.FlutterEngine\nimport io.flutter.plugin.common.MethodChannel\nimport android.appwidget.AppWidgetManager\nimport tech.lolli.toolbox.widget.HomeWidget\n\nclass MainActivity: FlutterFragmentActivity() {\n    private lateinit var channel: MethodChannel\n    private val ACTION_UPDATE_SESSIONS = \"tech.lolli.toolbox.ACTION_UPDATE_SESSIONS\"\n    private val ACTION_DISCONNECT_SESSION = \"tech.lolli.toolbox.ACTION_DISCONNECT_SESSION\"\n    private val ACTION_STOP_ALL_CONNECTIONS = \"tech.lolli.toolbox.STOP_ALL_CONNECTIONS\"\n    private var stopAllReceiver: BroadcastReceiver? = null\n\n    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\n        super.configureFlutterEngine(flutterEngine)\n        val binaryMessenger = flutterEngine.dartExecutor.binaryMessenger\n\n        channel = MethodChannel(binaryMessenger, \"tech.lolli.toolbox/main_chan\")\n        channel.setMethodCallHandler { method, result ->\n                when (method.method) {\n                    \"sendToBackground\" -> {\n                        moveTaskToBack(true)\n                        result.success(null)\n                    }\n                    \"isServiceRunning\" -> {\n                        result.success(ForegroundService.isRunning)\n                    }\n                    \"startService\" -> {\n                        try {\n                            reqPerm()\n                            if (!notificationsAllowed()) {\n                                // Don't start foreground service without notification permission on API 33+\n                                result.error(\"NOTIFICATION_PERMISSION_DENIED\", \"Notification permission not granted\", null)\n                                return@setMethodCallHandler\n                            }\n                            val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)\n                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                                startForegroundService(serviceIntent)\n                            } else {\n                                startService(serviceIntent)\n                            }\n                            result.success(null)\n                        } catch (e: Exception) {\n                            // Log error but don't crash\n                            android.util.Log.e(\"MainActivity\", \"Failed to start service: ${e.message}\")\n                            result.error(\"SERVICE_ERROR\", e.message, null)\n                        }\n                    }\n                    \"stopService\" -> {\n                        val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)\n                        stopService(serviceIntent)\n                        result.success(null)\n                    }\n                    \"updateHomeWidget\" -> {\n                        val intent = Intent(this@MainActivity, HomeWidget::class.java)\n                        intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE\n                        sendBroadcast(intent)\n                        result.success(null)\n                    }\n                    \"updateSessions\" -> {\n                        try {\n                            if (!notificationsAllowed()) {\n                                // Avoid starting/continuing service updates when notifications are blocked\n                                result.error(\"NOTIFICATION_PERMISSION_DENIED\", \"Notification permission not granted\", null)\n                                return@setMethodCallHandler\n                            }\n                            val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)\n                            serviceIntent.action = ACTION_UPDATE_SESSIONS\n                            serviceIntent.putExtra(\"payload\", method.arguments as String)\n                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                                startForegroundService(serviceIntent)\n                            } else {\n                                startService(serviceIntent)\n                            }\n                            result.success(null)\n                        } catch (e: Exception) {\n                            android.util.Log.e(\"MainActivity\", \"Failed to update sessions: ${e.message}\")\n                            result.error(\"SERVICE_ERROR\", e.message, null)\n                        }\n                    }\n                    else -> {\n                        result.notImplemented()\n                    }\n                }\n        }\n\n        // Handle intent if launched via notification action\n        handleActionIntent(intent)\n\n        // Register broadcast receiver for stop all connections\n        setupStopAllReceiver()\n    }\n\n    private fun reqPerm() {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return\n        \n        try {\n            // Check if we already have the permission to avoid unnecessary prompts\n            if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)\n                != PackageManager.PERMISSION_GRANTED) {\n                // Check if we should show rationale\n                if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) {\n                    android.util.Log.i(\"MainActivity\", \"User previously denied notification permission\")\n                }\n                \n                ActivityCompat.requestPermissions(\n                    this,\n                    arrayOf(Manifest.permission.POST_NOTIFICATIONS),\n                    123,\n                )\n            }\n        } catch (e: Exception) {\n            // Log error but don't crash\n            android.util.Log.e(\"MainActivity\", \"Failed to request permissions: ${e.message}\")\n        }\n    }\n\n    private fun notificationsAllowed(): Boolean {\n        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {\n            true\n        } else {\n            ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED\n        }\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        handleActionIntent(intent)\n    }\n\n    private fun handleActionIntent(intent: Intent?) {\n        if (intent == null) return\n        when (intent.action) {\n            ACTION_DISCONNECT_SESSION -> {\n                val sessionId = intent.getStringExtra(\"session_id\")\n                if (sessionId != null && ::channel.isInitialized) {\n                    try {\n                        channel.invokeMethod(\"disconnectSession\", mapOf(\"id\" to sessionId))\n                    } catch (e: Exception) {\n                        android.util.Log.e(\"MainActivity\", \"Failed to invoke disconnect: ${e.message}\")\n                    }\n                }\n            }\n        }\n    }\n\n    private fun setupStopAllReceiver() {\n        stopAllReceiver = object : BroadcastReceiver() {\n            override fun onReceive(context: Context?, intent: Intent?) {\n                if (intent?.action == ACTION_STOP_ALL_CONNECTIONS && ::channel.isInitialized) {\n                    try {\n                        channel.invokeMethod(\"stopAllConnections\", null)\n                    } catch (e: Exception) {\n                        android.util.Log.e(\"MainActivity\", \"Failed to invoke stopAllConnections: ${e.message}\")\n                    }\n                }\n            }\n        }\n        val filter = IntentFilter(ACTION_STOP_ALL_CONNECTIONS)\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            ContextCompat.registerReceiver(this, stopAllReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)\n        } else {\n            registerReceiver(stopAllReceiver, filter)\n        }\n    }\n\n    override fun onRequestPermissionsResult(\n        requestCode: Int,\n        permissions: Array<out String>,\n        grantResults: IntArray\n    ) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n        if (requestCode == 123) {\n            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {\n                android.util.Log.i(\"MainActivity\", \"Notification permission granted\")\n            } else {\n                android.util.Log.w(\"MainActivity\", \"Notification permission denied\")\n                // Optionally inform user about the limitation\n            }\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        stopAllReceiver?.let {\n            try {\n                unregisterReceiver(it)\n            } catch (e: Exception) {\n                android.util.Log.e(\"MainActivity\", \"Failed to unregister receiver: ${e.message}\")\n            }\n            stopAllReceiver = null\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/tech/lolli/toolbox/widget/HomeWidget.kt",
    "content": "package tech.lolli.toolbox.widget\n\nimport android.app.PendingIntent\nimport android.appwidget.AppWidgetManager\nimport android.appwidget.AppWidgetProvider\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport android.util.Log\nimport android.view.View\nimport android.widget.RemoteViews\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.withTimeoutOrNull\nimport org.json.JSONObject\nimport org.json.JSONException\nimport tech.lolli.toolbox.R\nimport java.net.URL\nimport java.net.HttpURLConnection\nimport java.net.SocketTimeoutException\nimport java.io.FileNotFoundException\nimport java.io.IOException\nimport java.util.concurrent.ConcurrentHashMap\n\nclass HomeWidget : AppWidgetProvider() {\n    companion object {\n        private const val TAG = \"HomeWidget\"\n        private const val NETWORK_TIMEOUT = 10_000L // 10 seconds\n        private const val COROUTINE_TIMEOUT = 15_000L // 15 seconds\n        private val activeUpdates = ConcurrentHashMap<Int, Boolean>()\n    }\n    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {\n        for (appWidgetId in appWidgetIds) {\n            updateAppWidget(context, appWidgetManager, appWidgetId)\n        }\n    }\n\n    private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {\n        // Prevent concurrent updates for the same widget\n        if (activeUpdates.putIfAbsent(appWidgetId, true) == true) {\n            Log.d(TAG, \"Widget $appWidgetId is already updating, skipping\")\n            return\n        }\n\n        val views = RemoteViews(context.packageName, R.layout.home_widget)\n        val url = getWidgetUrl(context, appWidgetId)\n\n        if (url.isNullOrEmpty()) {\n            Log.w(TAG, \"URL not found for widget $appWidgetId\")\n            showErrorState(views, appWidgetManager, appWidgetId, \"Please configure the widget URL.\")\n            activeUpdates.remove(appWidgetId)\n            return\n        }\n\n        setupClickIntent(context, views, appWidgetId)\n\n        showLoadingState(views, appWidgetManager, appWidgetId)\n\n        CoroutineScope(Dispatchers.IO).launch {\n            withTimeoutOrNull(COROUTINE_TIMEOUT) {\n                try {\n                    val serverData = fetchServerData(url)\n                    if (serverData != null) {\n                        withContext(Dispatchers.Main) {\n                            showSuccessState(views, appWidgetManager, appWidgetId, serverData)\n                        }\n                    } else {\n                        withContext(Dispatchers.Main) {\n                            showErrorState(views, appWidgetManager, appWidgetId, \"Invalid server data received.\")\n                        }\n                    }\n                } catch (e: Exception) {\n                    Log.e(TAG, \"Error updating widget $appWidgetId: ${e.message}\", e)\n                    withContext(Dispatchers.Main) {\n                        val errorMessage = when (e) {\n                            is SocketTimeoutException -> \"Connection timeout. Please check your network.\"\n                            is IOException -> \"Network error. Please check your connection.\"\n                            is JSONException -> \"Invalid data format received from server.\"\n                            else -> \"Failed to retrieve data: ${e.message}\"\n                        }\n                        showErrorState(views, appWidgetManager, appWidgetId, errorMessage)\n                    }\n                }\n            } ?: run {\n                Log.w(TAG, \"Widget update timed out for widget $appWidgetId\")\n                withContext(Dispatchers.Main) {\n                    showErrorState(views, appWidgetManager, appWidgetId, \"Update timed out. Please try again.\")\n                }\n            }\n            activeUpdates.remove(appWidgetId)\n        }\n    }\n\n    private fun getWidgetUrl(context: Context, appWidgetId: Int): String? {\n        val sp = context.getSharedPreferences(\"FlutterSharedPreferences\", Context.MODE_PRIVATE)\n        return sp.getString(\"widget_$appWidgetId\", null)\n            ?: sp.getString(\"$appWidgetId\", null)\n            ?: sp.getString(\"widget_*\", null)\n    }\n\n    private fun setupClickIntent(context: Context, views: RemoteViews, appWidgetId: Int) {\n        val intentConfigure = Intent(context, WidgetConfigureActivity::class.java).apply {\n            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)\n        }\n\n        val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        } else {\n            PendingIntent.FLAG_UPDATE_CURRENT\n        }\n\n        val pendingConfigure = PendingIntent.getActivity(context, appWidgetId, intentConfigure, flag)\n        views.setOnClickPendingIntent(R.id.widget_container, pendingConfigure)\n    }\n\n    private suspend fun fetchServerData(url: String): ServerData? = withContext(Dispatchers.IO) {\n        var connection: HttpURLConnection? = null\n        try {\n            connection = (URL(url).openConnection() as HttpURLConnection).apply {\n                requestMethod = \"GET\"\n                connectTimeout = NETWORK_TIMEOUT.toInt()\n                readTimeout = NETWORK_TIMEOUT.toInt()\n                setRequestProperty(\"User-Agent\", \"ServerBox-Widget/1.0\")\n                setRequestProperty(\"Accept\", \"application/json\")\n            }\n\n            if (connection.responseCode != HttpURLConnection.HTTP_OK) {\n                throw IOException(\"HTTP ${connection.responseCode}: ${connection.responseMessage}\")\n            }\n\n            val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }\n            parseServerData(jsonStr)\n        } finally {\n            connection?.disconnect()\n        }\n    }\n\n    private fun parseServerData(jsonStr: String): ServerData? {\n        return try {\n            val jsonObject = JSONObject(jsonStr)\n            val data = jsonObject.getJSONObject(\"data\")\n            \n            val server = data.optString(\"name\", \"Unknown Server\")\n            val cpu = data.optString(\"cpu\", \"\").takeIf { it.isNotBlank() } ?: \"N/A\"\n            val mem = data.optString(\"mem\", \"\").takeIf { it.isNotBlank() } ?: \"N/A\"\n            val disk = data.optString(\"disk\", \"\").takeIf { it.isNotBlank() } ?: \"N/A\"\n            val net = data.optString(\"net\", \"\").takeIf { it.isNotBlank() } ?: \"N/A\"\n            \n            // Return data even if some fields are missing, providing defaults\n            // Only reject if we can't parse the JSON structure properly\n            ServerData(server, cpu, mem, disk, net)\n        } catch (e: JSONException) {\n            Log.e(TAG, \"JSON parsing error: ${e.message}\", e)\n            null\n        }\n    }\n\n    private fun showLoadingState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int) {\n        views.apply {\n            setTextViewText(R.id.widget_name, \"Loading...\")\n            setViewVisibility(R.id.error_message, View.GONE)\n            setViewVisibility(R.id.widget_content, View.VISIBLE)\n            setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)\n            setViewVisibility(R.id.widget_mem_label, View.VISIBLE)\n            setViewVisibility(R.id.widget_disk_label, View.VISIBLE)\n            setViewVisibility(R.id.widget_net_label, View.VISIBLE)\n            setViewVisibility(R.id.widget_progress, View.VISIBLE)\n            setFloat(R.id.widget_name, \"setAlpha\", 0.7f)\n        }\n        appWidgetManager.updateAppWidget(appWidgetId, views)\n    }\n\n    private fun showSuccessState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, data: ServerData) {\n        views.apply {\n            setTextViewText(R.id.widget_name, data.name)\n            setTextViewText(R.id.widget_cpu, data.cpu)\n            setTextViewText(R.id.widget_mem, data.mem)\n            setTextViewText(R.id.widget_disk, data.disk)\n            setTextViewText(R.id.widget_net, data.net)\n            \n            val timeStr = android.text.format.DateFormat.format(\"HH:mm\", java.util.Date()).toString()\n            setTextViewText(R.id.widget_time, timeStr)\n            \n            setViewVisibility(R.id.error_message, View.GONE)\n            setViewVisibility(R.id.widget_content, View.VISIBLE)\n            setViewVisibility(R.id.widget_progress, View.GONE)\n            \n            // Smooth fade-in animation\n            setFloat(R.id.widget_name, \"setAlpha\", 1f)\n            setFloat(R.id.widget_cpu_label, \"setAlpha\", 1f)\n            setFloat(R.id.widget_mem_label, \"setAlpha\", 1f)\n            setFloat(R.id.widget_disk_label, \"setAlpha\", 1f)\n            setFloat(R.id.widget_net_label, \"setAlpha\", 1f)\n            setFloat(R.id.widget_time, \"setAlpha\", 1f)\n        }\n        appWidgetManager.updateAppWidget(appWidgetId, views)\n    }\n\n    private fun showErrorState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, errorMessage: String) {\n        views.apply {\n            setTextViewText(R.id.widget_name, \"Error\")\n            setViewVisibility(R.id.error_message, View.VISIBLE)\n            setTextViewText(R.id.error_message, errorMessage)\n            setViewVisibility(R.id.widget_content, View.GONE)\n            setViewVisibility(R.id.widget_progress, View.GONE)\n            setFloat(R.id.widget_name, \"setAlpha\", 1f)\n            setFloat(R.id.error_message, \"setAlpha\", 1f)\n        }\n        appWidgetManager.updateAppWidget(appWidgetId, views)\n    }\n\n    data class ServerData(\n        val name: String,\n        val cpu: String,\n        val mem: String,\n        val disk: String,\n        val net: String\n    )\n}"
  },
  {
    "path": "android/app/src/main/kotlin/tech/lolli/toolbox/widget/WidgetConfigureActivity.kt",
    "content": "package tech.lolli.toolbox.widget\n\nimport android.app.Activity\nimport android.appwidget.AppWidgetManager\nimport android.content.Intent\nimport android.os.Bundle\nimport android.util.Patterns\nimport android.widget.Button\nimport android.widget.EditText\nimport tech.lolli.toolbox.R\n\nclass WidgetConfigureActivity : Activity() {\n    private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID\n    private lateinit var urlEditText: EditText\n    private lateinit var saveButton: Button\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.widget_configure)\n\n        // 设置结果为取消，以防用户在完成配置前退出\n        setResult(RESULT_CANCELED)\n\n        // 获取 widget ID\n        val extras = intent.extras\n        if (extras != null) {\n            appWidgetId = extras.getInt(\n                AppWidgetManager.EXTRA_APPWIDGET_ID,\n                AppWidgetManager.INVALID_APPWIDGET_ID\n            )\n        }\n\n        // 如果没有有效的 widget ID，完成 activity\n        if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {\n            finish()\n            return\n        }\n\n        // 初始化 UI 元素\n        urlEditText = findViewById(R.id.url_edit_text)\n        saveButton = findViewById(R.id.save_button)\n\n        // 从 SharedPreferences 加载现有配置\n        val sp = getSharedPreferences(\"FlutterSharedPreferences\", MODE_PRIVATE)\n        val existingUrl = sp.getString(\"widget_$appWidgetId\", \"\")\n        urlEditText.setText(existingUrl)\n\n        // 设置保存按钮点击事件\n        saveButton.setOnClickListener {\n            val url = urlEditText.text.toString().trim()\n            if (url.isEmpty()) {\n                urlEditText.error = \"Please enter a URL\"\n                return@setOnClickListener\n            }\n            \n            // 验证 URL 格式\n            if (!Patterns.WEB_URL.matcher(url).matches()) {\n                urlEditText.error = \"Please enter a valid URL\"\n                return@setOnClickListener\n            }\n\n            // 保存 URL 到 SharedPreferences\n            val editor = sp.edit()\n            editor.putString(\"widget_$appWidgetId\", url)\n            editor.apply()\n\n            // 更新 widget 使用 AppWidgetManager\n            val appWidgetManager = AppWidgetManager.getInstance(this)\n            val updateIntent = Intent(this, HomeWidget::class.java).apply {\n                action = AppWidgetManager.ACTION_APPWIDGET_UPDATE\n                putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))\n            }\n            sendBroadcast(updateIntent)\n\n            // 设置结果并结束 activity\n            val resultValue = Intent()\n            resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)\n            setResult(RESULT_OK, resultValue)\n            finish()\n        }\n    }\n}"
  },
  {
    "path": "android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <bitmap android:gravity=\"fill\" android:src=\"@drawable/background\"/>\n    </item>\n    <item>\n        <bitmap android:gravity=\"center\" android:src=\"@drawable/splash\"/>\n    </item>\n</layer-list>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/memory_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"@color/widgetSummaryText\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M15,9L9,9v6h6L15,9zM13,13h-2v-2h2v2zM21,11L21,9h-2L19,7c0,-1.1 -0.9,-2 -2,-2h-2L15,3h-2v2h-2L11,3L9,3v2L7,5c-1.1,0 -2,0.9 -2,2v2L3,9v2h2v2L3,13v2h2v2c0,1.1 0.9,2 2,2h2v2h2v-2h2v2h2v-2h2c1.1,0 2,-0.9 2,-2v-2h2v-2h-2v-2h2zM17,17L7,17L7,7h10v10z\"/>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/net_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"@color/widgetSummaryText\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M7.77,6.76L6.23,5.48 0.82,12l5.41,6.52 1.54,-1.28L3.42,12l4.35,-5.24zM7,13h2v-2L7,11v2zM17,11h-2v2h2v-2zM11,13h2v-2h-2v2zM17.77,5.48l-1.54,1.28L20.58,12l-4.35,5.24 1.54,1.28L23.18,12l-5.41,-6.52z\"/>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/settings_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"@color/widgetSummaryText\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z\"/>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/speed_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"@color/widgetSummaryText\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M20.38,8.57l-1.23,1.85a8,8 0,0 1,-0.22 7.58L5.07,18A8,8 0,0 1,15.58 6.85l1.85,-1.23A10,10 0,0 0,3.35 19a2,2 0,0 0,1.72 1h13.85a2,2 0,0 0,1.74 -1,10 10,0 0,0 -0.27,-10.44zM10.59,15.41a2,2 0,0 0,2.83 0l5.66,-8.49 -8.49,5.66a2,2 0,0 0,0 2.83z\"/>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/storage_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"@color/widgetSummaryText\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M2,20h20v-4L2,16v4zM4,17h2v2L4,19v-2zM2,4v4h20L22,4L2,4zM6,7L4,7L4,5h2v2zM2,14h20v-4L2,10v4zM4,11h2v2L4,13v-2z\"/>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/widget_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <corners android:radius=\"16dp\"/>\n    <solid android:color=\"@color/widgetBackground\" />\n</shape>"
  },
  {
    "path": "android/app/src/main/res/drawable-night/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <bitmap android:gravity=\"fill\" android:src=\"@drawable/background\"/>\n    </item>\n    <item>\n        <bitmap android:gravity=\"center\" android:src=\"@drawable/splash\"/>\n    </item>\n</layer-list>\n"
  },
  {
    "path": "android/app/src/main/res/drawable-night-v21/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <bitmap android:gravity=\"fill\" android:src=\"@drawable/background\"/>\n    </item>\n    <item>\n        <bitmap android:gravity=\"center\" android:src=\"@drawable/splash\"/>\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<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <bitmap android:gravity=\"fill\" android:src=\"@drawable/background\"/>\n    </item>\n    <item>\n        <bitmap android:gravity=\"center\" android:src=\"@drawable/splash\"/>\n    </item>\n</layer-list>\n"
  },
  {
    "path": "android/app/src/main/res/layout/home_widget.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/widget_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@drawable/widget_background\"\n    android:padding=\"17dp\"\n    android:orientation=\"vertical\" >\n\n    <TextView\n        android:id=\"@+id/widget_name\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textColor=\"@color/widgetText\"\n        android:textSize=\"20sp\"\n        android:textStyle=\"bold\"\n        android:maxLines=\"1\"\n        android:ellipsize=\"end\"\n        android:alpha=\"0\"\n        android:animateLayoutChanges=\"true\"\n        android:fadingEdge=\"horizontal\"\n        android:singleLine=\"true\"\n        tools:text=\"Server Name\" />\n\n    <!-- Wrap the content in a LinearLayout for easy visibility management -->\n    <LinearLayout\n        android:id=\"@+id/widget_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:layout_below=\"@id/widget_name\"\n        android:layout_marginTop=\"8dp\">\n\n        <RelativeLayout\n            android:id=\"@+id/widget_container_inner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:animateLayoutChanges=\"true\">\n\n            <LinearLayout\n                android:id=\"@+id/widget_cpu_label\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"4dp\"\n                android:gravity=\"center_vertical\"\n                android:orientation=\"horizontal\"\n                android:alpha=\"0\"\n                android:animateLayoutChanges=\"true\">\n\n                <ImageView\n                    android:layout_width=\"16dp\"\n                    android:layout_height=\"16dp\"\n                    android:src=\"@drawable/speed_24\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:contentDescription=\"CPU usage\" />\n\n                <TextView\n                    android:id=\"@+id/widget_cpu\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:layout_marginStart=\"8dp\"\n                    android:singleLine=\"true\"\n                    android:ellipsize=\"end\"\n                    android:textColor=\"@color/widgetSummaryText\"\n                    android:textSize=\"12sp\"\n                    tools:text=\"CPU: 25.6%\" />\n\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/widget_mem_label\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"4dp\"\n                android:layout_below=\"@id/widget_cpu_label\"\n                android:gravity=\"center_vertical\"\n                android:orientation=\"horizontal\"\n                android:alpha=\"0\"\n                android:animateLayoutChanges=\"true\">\n\n                <ImageView\n                    android:layout_width=\"16dp\"\n                    android:layout_height=\"16dp\"\n                    android:src=\"@drawable/memory_24\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:contentDescription=\"Memory usage\" />\n\n                <TextView\n                    android:id=\"@+id/widget_mem\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:layout_marginStart=\"8dp\"\n                    android:maxLines=\"1\"\n                    android:ellipsize=\"end\"\n                    android:textColor=\"@color/widgetSummaryText\"\n                    android:textSize=\"12sp\"\n                    tools:text=\"Memory: 4.2GB / 8GB\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/widget_disk_label\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"4dp\"\n                android:layout_below=\"@id/widget_mem_label\"\n                android:gravity=\"center_vertical\"\n                android:orientation=\"horizontal\"\n                android:alpha=\"0\"\n                android:animateLayoutChanges=\"true\">\n\n                <ImageView\n                    android:layout_width=\"16dp\"\n                    android:layout_height=\"16dp\"\n                    android:src=\"@drawable/storage_24\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:contentDescription=\"Disk usage\" />\n\n                <TextView\n                    android:id=\"@+id/widget_disk\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:layout_marginStart=\"8dp\"\n                    android:maxLines=\"1\"\n                    android:ellipsize=\"end\"\n                    android:textColor=\"@color/widgetSummaryText\"\n                    android:textSize=\"12sp\"\n                    tools:text=\"Disk: 125GB / 250GB\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/widget_net_label\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_below=\"@id/widget_disk_label\"\n                android:gravity=\"center_vertical\"\n                android:orientation=\"horizontal\"\n                android:alpha=\"0\"\n                android:animateLayoutChanges=\"true\">\n\n                <ImageView\n                    android:layout_width=\"16dp\"\n                    android:layout_height=\"16dp\"\n                    android:src=\"@drawable/net_24\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:contentDescription=\"Network usage\" />\n\n                <TextView\n                    android:id=\"@+id/widget_net\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:layout_marginStart=\"8dp\"\n                    android:maxLines=\"1\"\n                    android:ellipsize=\"end\"\n                    android:textColor=\"@color/widgetSummaryText\"\n                    android:textSize=\"12sp\"\n                    tools:text=\"Network: 15MB/s ↓ 8MB/s ↑\" />\n\n            </LinearLayout>\n\n        </RelativeLayout>\n\n    </LinearLayout>\n\n    <!-- Error message display -->\n    <TextView\n        android:id=\"@+id/error_message\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/widget_name\"\n        android:layout_marginTop=\"8dp\"\n        android:textColor=\"@color/widgetSummaryText\"\n        android:textSize=\"11sp\"\n        android:visibility=\"gone\"\n        android:alpha=\"0\"\n        android:animateLayoutChanges=\"true\"\n        android:lineSpacingMultiplier=\"1.2\"\n        android:maxLines=\"3\"\n        android:ellipsize=\"end\"\n        tools:text=\"Error message text that might be longer than usual\" />\n\n    <TextView\n        android:id=\"@+id/widget_time\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentBottom=\"true\"\n        android:layout_alignParentEnd=\"true\"\n        android:maxLines=\"1\"\n        android:textColor=\"@color/widgetSummaryText\"\n        android:textSize=\"10sp\"\n        android:alpha=\"0\"\n        android:animateLayoutChanges=\"true\"\n        android:fontFamily=\"monospace\"\n        tools:text=\"12:34\" />\n\n    <!-- Progress indicator for loading state -->\n    <ProgressBar\n        android:id=\"@+id/widget_progress\"\n        style=\"?android:attr/progressBarStyleLarge\"\n        android:layout_width=\"32dp\"\n        android:layout_height=\"32dp\"\n        android:layout_centerInParent=\"true\"\n        android:visibility=\"gone\"\n        android:indeterminate=\"true\" />\n\n</RelativeLayout>"
  },
  {
    "path": "android/app/src/main/res/layout/widget_configure.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:padding=\"16dp\">\n\n    <TextView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"Widget URL\"\n        android:textSize=\"18sp\"\n        android:textStyle=\"bold\"\n        android:layout_marginBottom=\"16dp\"\n        android:textColor=\"@android:color/black\" />\n\n    <EditText\n        android:id=\"@+id/url_edit_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:hint=\"https://server/status\"\n        android:inputType=\"textUri\"\n        android:layout_marginBottom=\"16dp\"\n        android:background=\"@android:drawable/edit_text\"\n        android:padding=\"12dp\"\n        android:textColor=\"@android:color/black\"\n        android:textColorHint=\"@android:color/darker_gray\" />\n\n    <Button\n        android:id=\"@+id/save_button\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"Save\"\n        android:background=\"#8b2252\"\n        android:textColor=\"@android:color/white\"\n        android:padding=\"12dp\" />\n\n</LinearLayout>"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@mipmap/ic_launcher_monochrome\" />\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<resources>\n    <color name=\"colorLaunch\">#EEEEEE</color>\n    <color name=\"widgetBackground\">#FFFFFF</color>\n    <color name=\"widgetText\">#000000</color>\n    <color name=\"widgetSummaryText\">#333333</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#FFFFFF</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\">ServerBox</string>\n</resources>"
  },
  {
    "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             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n        <item name=\"android:forceDarkAllowed\">false</item>\n        <item name=\"android:windowFullscreen\">false</item>\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">false</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</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</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-night/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"widgetBackground\">#000000</color>\n    <color name=\"widgetText\">#FFFFFF</color>\n    <color name=\"widgetSummaryText\">#BBBBBB</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values-night/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#372D2D</color>\n</resources>"
  },
  {
    "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             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n        <item name=\"android:forceDarkAllowed\">false</item>\n        <item name=\"android:windowFullscreen\">false</item>\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">false</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</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/main/res/xml/backup_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<full-backup-content>\n    <exclude domain=\"sharedpref\" path=\"FlutterSecureStorage\"/>\n</full-backup-content>"
  },
  {
    "path": "android/app/src/main/res/xml/home_widget.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<appwidget-provider xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:minWidth=\"110dp\"\n    android:maxHeight=\"110dp\"\n    android:maxWidth=\"110dp\"\n    android:minHeight=\"110dp\"\n    android:updatePeriodMillis=\"1800001\"\n    android:initialLayout=\"@layout/home_widget\"\n    android:configure=\"tech.lolli.toolbox.widget.WidgetConfigureActivity\"\n    android:resizeMode=\"none\"\n    android:widgetCategory=\"home_screen\">\n</appwidget-provider>"
  },
  {
    "path": "android/build.gradle",
    "content": "allprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.buildDir = '../build'\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n}\n\nsubprojects { subproject ->\n  // Only works on com.android.application(the main app module)\n  if (subproject.plugins.hasPlugin('com.android.application')) {\n    subproject.afterEvaluate {\n      android.buildTypes.matching { it.name == 'profile' }.all { buildType ->\n        buildType.applicationIdSuffix = \".profile\"\n        buildTypes.profile.resValue 'string', 'app_name', 'SrvBxP'\n      }\n      android.buildTypes.matching { it.name == 'debug' }.all { buildType ->\n        buildType.applicationIdSuffix = \".debug\"\n        buildTypes.debug.resValue 'string', 'app_name', 'SrvBxD'\n      }\n    }\n  }\n}\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": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.14.3-all.zip\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx4G\nandroid.useAndroidX=true\nandroid.enableJetifier=true\nandroid.defaults.buildfeatures.buildconfig=true\nandroid.nonTransitiveRClass=false\nandroid.nonFinalResIds=false\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "pluginManagement {\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.21\" apply false\n}\n\ninclude \":app\"\n"
  },
  {
    "path": "coverage/lcov.info",
    "content": "SF:lib/data/model/app/shell_func.dart\nDA:32,3\nDA:38,1\nDA:39,1\nDA:42,1\nDA:43,2\nDA:44,2\nDA:47,0\nDA:48,0\nDA:49,0\nDA:50,0\nDA:51,0\nDA:52,0\nDA:55,1\nDA:56,1\nDA:57,1\nDA:58,1\nDA:59,1\nDA:62,1\nDA:63,1\nDA:64,1\nDA:65,1\nDA:67,2\nDA:69,1\nDA:72,1\nDA:73,1\nDA:74,1\nDA:75,1\nDA:76,1\nDA:77,1\nDA:81,1\nDA:82,1\nDA:83,1\nDA:84,1\nDA:86,1\nDA:92,1\nDA:93,1\nDA:94,1\nDA:96,1\nDA:159,0\nDA:160,0\nDA:161,0\nDA:162,0\nDA:163,0\nDA:164,0\nDA:165,0\nDA:166,0\nDA:239,1\nDA:240,2\nLF:48\nLH:34\nend_of_record\nSF:lib/data/model/server/server_status_update_req.dart\nDA:26,1\nDA:34,1\nDA:35,1\nDA:36,1\nDA:37,1\nDA:38,2\nDA:44,0\nDA:45,0\nDA:47,0\nDA:48,0\nDA:51,0\nDA:52,0\nDA:54,0\nDA:58,0\nDA:60,0\nDA:63,0\nDA:67,0\nDA:69,0\nDA:72,0\nDA:76,0\nDA:77,0\nDA:78,0\nDA:79,0\nDA:80,0\nDA:82,0\nDA:86,0\nDA:87,0\nDA:88,0\nDA:91,0\nDA:95,0\nDA:97,0\nDA:100,0\nDA:104,0\nDA:105,0\nDA:107,0\nDA:111,0\nDA:113,0\nDA:117,0\nDA:119,0\nDA:122,0\nDA:126,0\nDA:128,0\nDA:132,0\nDA:133,0\nDA:135,0\nDA:139,0\nDA:140,0\nDA:142,0\nDA:146,0\nDA:148,0\nDA:152,0\nDA:154,0\nDA:158,0\nDA:161,0\nDA:162,0\nDA:163,0\nDA:164,0\nDA:167,0\nDA:171,0\nDA:172,0\nDA:173,0\nDA:174,0\nDA:177,0\nDA:181,0\nDA:182,0\nDA:183,0\nDA:184,0\nDA:187,0\nDA:190,0\nDA:194,0\nDA:195,0\nDA:198,0\nDA:199,0\nDA:200,0\nDA:202,0\nDA:206,0\nDA:208,0\nDA:212,0\nDA:214,0\nDA:218,0\nDA:220,0\nDA:224,0\nDA:226,0\nDA:229,0\nDA:233,0\nDA:235,0\nDA:237,0\nDA:245,0\nDA:246,0\nDA:247,0\nDA:248,0\nDA:249,0\nDA:251,0\nDA:254,0\nDA:257,0\nDA:258,0\nDA:259,0\nDA:261,0\nDA:262,0\nDA:263,0\nDA:264,0\nDA:271,0\nDA:272,0\nDA:273,0\nDA:278,0\nDA:288,0\nDA:289,0\nDA:290,0\nDA:291,0\nDA:296,1\nDA:297,1\nDA:298,1\nDA:303,1\nDA:304,1\nDA:305,2\nDA:306,3\nDA:309,1\nDA:310,1\nDA:311,1\nDA:312,1\nDA:313,1\nDA:314,1\nDA:315,1\nDA:316,1\nDA:317,1\nDA:318,1\nDA:319,1\nDA:320,1\nDA:321,5\nDA:323,1\nDA:327,1\nDA:329,1\nDA:330,1\nDA:331,1\nDA:332,1\nDA:333,1\nDA:334,0\nDA:335,0\nDA:336,0\nDA:337,0\nDA:341,0\nDA:346,1\nDA:348,1\nDA:349,1\nDA:350,3\nDA:353,0\nDA:358,1\nDA:360,2\nDA:362,3\nDA:365,3\nDA:370,1\nDA:373,1\nDA:374,1\nDA:375,1\nDA:376,1\nDA:377,1\nDA:378,2\nDA:379,1\nDA:380,3\nDA:385,1\nDA:386,2\nDA:387,4\nDA:388,5\nDA:391,3\nDA:396,1\nDA:398,1\nDA:399,1\nDA:400,1\nDA:401,1\nDA:402,1\nDA:403,1\nDA:405,2\nDA:409,3\nDA:414,1\nDA:416,1\nDA:417,2\nDA:418,1\nDA:419,2\nDA:420,3\nDA:423,3\nDA:428,1\nDA:430,2\nDA:432,3\nDA:435,3\nDA:440,1\nDA:442,1\nDA:443,2\nDA:444,1\nDA:445,3\nDA:448,3\nDA:453,1\nDA:455,1\nDA:456,2\nDA:458,3\nDA:461,3\nDA:466,1\nDA:468,1\nDA:469,2\nDA:470,1\nDA:471,3\nDA:472,1\nDA:473,3\nDA:477,3\nDA:482,1\nDA:484,1\nDA:485,2\nDA:486,3\nDA:489,3\nDA:494,1\nDA:496,3\nDA:498,3\nDA:502,4\nDA:504,3\nDA:509,1\nDA:511,1\nDA:512,1\nDA:514,2\nDA:516,2\nDA:518,1\nDA:519,1\nDA:524,1\nDA:525,0\nDA:526,0\nDA:528,1\nDA:529,1\nDA:541,1\nDA:545,0\nDA:547,0\nDA:548,0\nDA:551,0\nDA:552,0\nDA:553,0\nDA:555,0\nDA:556,0\nDA:558,0\nDA:559,0\nDA:560,0\nDA:562,0\nDA:563,0\nDA:564,0\nDA:565,0\nDA:567,0\nDA:568,0\nDA:569,0\nDA:570,0\nDA:576,0\nDA:577,0\nDA:578,0\nDA:580,0\nDA:581,0\nDA:583,0\nDA:584,0\nDA:594,0\nDA:598,0\nDA:601,0\nDA:602,0\nDA:605,1\nDA:607,1\nDA:608,0\nDA:611,0\nDA:612,0\nDA:614,0\nDA:615,0\nDA:617,0\nDA:618,0\nDA:619,0\nDA:621,0\nDA:622,0\nDA:623,0\nDA:624,0\nDA:626,0\nDA:627,0\nDA:628,0\nDA:629,0\nDA:636,0\nDA:637,0\nDA:638,0\nDA:639,0\nDA:640,0\nDA:642,0\nDA:643,0\nDA:656,1\nDA:660,0\nDA:663,0\nDA:664,0\nDA:667,1\nDA:670,1\nDA:671,1\nDA:672,1\nDA:676,1\nDA:677,0\nDA:680,0\nDA:681,0\nDA:683,0\nDA:684,0\nDA:685,0\nDA:686,0\nDA:690,0\nDA:693,0\nDA:697,0\nDA:698,0\nLF:301\nLH:123\nend_of_record\nSF:lib/data/model/server/system.dart\nDA:22,0\nDA:24,0\nDA:25,0\nDA:26,0\nDA:28,0\nDA:29,0\nDA:32,0\nDA:33,0\nDA:38,0\nDA:39,0\nDA:43,0\nDA:44,0\nDA:50,0\nDA:56,3\nDA:58,1\nDA:60,1\nDA:61,0\nDA:62,1\nDA:63,0\nDA:64,1\nDA:65,1\nLF:21\nLH:6\nend_of_record\nSF:lib/data/res/status.dart\nDA:11,1\nDA:12,1\nDA:13,1\nDA:14,5\nDA:15,1\nDA:16,3\nDA:17,1\nDA:18,5\nDA:19,2\nDA:20,1\nDA:22,1\nDA:23,1\nDA:27,1\nDA:28,1\nDA:29,1\nDA:33,1\nDA:36,1\nDA:37,3\nLF:18\nLH:18\nend_of_record\nSF:lib/data/model/server/disk.dart\nDA:30,3\nDA:44,2\nDA:45,2\nDA:46,2\nDA:48,2\nDA:50,2\nDA:51,2\nDA:53,4\nDA:55,2\nDA:59,1\nDA:62,0\nDA:68,2\nDA:69,2\nDA:71,2\nDA:76,3\nDA:77,4\nDA:78,4\nDA:79,4\nDA:82,3\nDA:83,1\nDA:85,1\nDA:92,1\nDA:93,2\nDA:94,2\nDA:95,2\nDA:97,1\nDA:101,1\nDA:105,2\nDA:106,3\nDA:108,2\nDA:109,3\nDA:111,2\nDA:112,3\nDA:115,2\nDA:116,1\nDA:117,1\nDA:119,2\nDA:120,2\nDA:121,2\nDA:123,1\nDA:138,2\nDA:139,4\nDA:140,4\nDA:143,4\nDA:145,2\nDA:148,4\nDA:149,4\nDA:150,2\nDA:152,2\nDA:157,6\nDA:158,4\nDA:159,6\nDA:161,4\nDA:162,6\nDA:164,4\nDA:165,6\nDA:168,4\nDA:169,2\nDA:170,2\nDA:172,4\nDA:173,4\nDA:174,4\nDA:176,2\nDA:189,1\nDA:192,0\nDA:193,0\nDA:201,1\nDA:202,1\nDA:203,1\nDA:204,2\nDA:206,2\nDA:207,1\nDA:210,2\nDA:211,2\nDA:212,0\nDA:215,1\nDA:216,0\nDA:220,1\nDA:221,1\nDA:222,1\nDA:223,1\nDA:224,1\nDA:227,3\nDA:228,4\nDA:229,4\nDA:230,4\nDA:240,0\nDA:241,0\nDA:242,0\nDA:243,0\nDA:244,0\nDA:245,0\nDA:246,0\nDA:247,0\nDA:248,0\nDA:249,0\nDA:250,0\nDA:251,0\nDA:252,0\nDA:257,1\nDA:259,1\nDA:261,2\nDA:264,0\nDA:267,0\nDA:268,0\nDA:272,0\nDA:273,0\nDA:276,0\nDA:277,0\nDA:278,0\nDA:279,0\nDA:280,0\nDA:284,0\nDA:285,0\nDA:287,0\nDA:288,0\nDA:293,1\nDA:294,2\nDA:296,0\nDA:299,0\nDA:300,0\nDA:301,0\nDA:302,0\nDA:303,0\nDA:304,0\nDA:307,0\nDA:308,0\nDA:309,0\nDA:312,0\nDA:313,0\nDA:317,0\nDA:318,0\nDA:319,0\nDA:320,0\nDA:321,0\nDA:322,0\nDA:323,0\nDA:324,0\nDA:325,0\nDA:327,0\nDA:328,0\nDA:329,0\nDA:330,0\nDA:332,0\nDA:333,0\nDA:351,0\nDA:353,0\nDA:354,0\nDA:361,3\nDA:363,1\nDA:365,3\nDA:366,4\nDA:370,3\nDA:372,3\nDA:373,3\nDA:374,6\nDA:375,9\nDA:378,9\nDA:379,3\nDA:380,3\nDA:381,6\nDA:382,6\nDA:384,3\nDA:388,3\nDA:393,3\nDA:395,3\nDA:396,3\nDA:404,6\nLF:168\nLH:106\nend_of_record\nSF:lib/data/model/server/time_seq.dart\nDA:8,2\nDA:10,1\nDA:12,2\nDA:13,4\nDA:14,0\nDA:16,2\nDA:19,1\nDA:20,2\nDA:22,2\nDA:24,0\nDA:26,0\nDA:29,0\nDA:31,0\nDA:34,0\nDA:36,0\nDA:43,3\nDA:45,1\nDA:46,4\nDA:49,1\nDA:50,4\nDA:55,1\nDA:56,1\nDA:58,5\nDA:59,0\nDA:60,0\nDA:63,1\nLF:26\nLH:17\nend_of_record\nSF:lib/data/res/misc.dart\nDA:4,3\nDA:5,3\nDA:8,0\nLF:3\nLH:2\nend_of_record\nSF:lib/data/model/container/image.dart\nDA:13,0\nDA:27,0\nDA:29,0\nDA:30,0\nDA:32,0\nDA:33,0\nDA:35,0\nDA:37,0\nDA:39,0\nDA:40,0\nDA:41,0\nDA:42,0\nDA:43,0\nDA:44,0\nDA:45,0\nDA:48,0\nDA:49,0\nDA:50,0\nDA:51,0\nDA:52,0\nDA:53,0\nDA:54,0\nDA:69,0\nDA:78,0\nDA:79,0\nDA:81,0\nDA:82,0\nDA:84,0\nDA:86,0\nDA:88,0\nDA:89,0\nDA:90,0\nDA:91,0\nDA:93,0\nDA:94,0\nDA:95,0\nDA:96,0\nDA:98,0\nDA:99,0\nDA:100,0\nDA:101,0\nDA:103,0\nDA:105,0\nDA:106,0\nDA:109,0\nDA:113,0\nDA:114,0\nDA:115,0\nDA:116,0\nDA:117,0\nDA:118,0\nDA:119,0\nLF:52\nLH:0\nend_of_record\nSF:lib/data/model/container/type.dart\nDA:8,0\nDA:9,0\nDA:10,0\nDA:13,0\nDA:14,0\nDA:15,0\nLF:6\nLH:0\nend_of_record\nSF:lib/data/model/container/ps.dart\nDA:20,0\nDA:45,0\nDA:47,0\nDA:48,0\nDA:50,0\nDA:51,0\nDA:53,0\nDA:54,0\nDA:56,0\nDA:58,0\nDA:59,0\nDA:60,0\nDA:61,0\nDA:62,0\nDA:63,0\nDA:64,0\nDA:65,0\nDA:66,0\nDA:67,0\nDA:68,0\nDA:69,0\nDA:70,0\nDA:73,0\nDA:75,0\nDA:77,0\nDA:78,0\nDA:79,0\nDA:80,0\nDA:81,0\nDA:82,0\nDA:83,0\nDA:84,0\nDA:87,0\nDA:88,0\nDA:89,0\nDA:90,0\nDA:91,0\nDA:92,0\nDA:93,0\nDA:94,0\nDA:115,1\nDA:117,0\nDA:118,0\nDA:120,0\nDA:123,1\nDA:125,3\nDA:129,0\nDA:131,0\nDA:132,0\nDA:133,0\nDA:134,0\nDA:135,0\nDA:140,1\nDA:141,2\nDA:142,6\nLF:55\nLH:6\nend_of_record\nSF:lib/core/extension/context/locale.dart\nDA:5,0\nDA:8,0\nLF:2\nLH:0\nend_of_record\nSF:lib/core/extension/ssh_client.dart\nDA:18,0\nDA:27,0\nDA:33,0\nDA:34,0\nDA:35,0\nDA:37,0\nDA:38,0\nDA:39,0\nDA:40,0\nDA:42,0\nDA:43,0\nDA:46,0\nDA:47,0\nDA:48,0\nDA:49,0\nDA:51,0\nDA:52,0\nDA:55,0\nDA:57,0\nDA:58,0\nDA:60,0\nDA:63,0\nDA:74,0\nDA:77,0\nDA:84,0\nDA:85,0\nDA:86,0\nDA:88,0\nDA:89,0\nDA:90,0\nDA:91,0\nDA:93,0\nDA:94,0\nDA:97,0\nDA:98,0\nDA:99,0\nDA:100,0\nDA:102,0\nDA:103,0\nDA:106,0\nDA:108,0\nDA:109,0\nDA:111,0\nDA:114,0\nDA:123,0\nDA:124,0\nDA:125,0\nDA:126,0\nDA:128,0\nDA:129,0\nDA:132,0\nDA:134,0\nDA:136,0\nDA:137,0\nDA:138,0\nDA:140,0\nDA:148,0\nDA:151,0\nDA:159,0\nDA:160,0\nDA:161,0\nDA:162,0\nLF:62\nLH:0\nend_of_record\nSF:lib/core/sync.dart\nDA:9,0\nDA:12,6\nDA:14,0\nDA:15,0\nDA:17,0\nDA:19,0\nDA:20,0\nDA:23,0\nDA:25,0\nDA:26,0\nDA:28,0\nDA:29,0\nLF:12\nLH:1\nend_of_record\nSF:lib/data/model/app/bak/backup2.dart\nDA:13,0\nDA:17,0\nDA:41,0\nDA:43,0\nDA:45,0\nDA:48,0\nDA:49,0\nDA:50,0\nDA:51,0\nDA:52,0\nDA:53,0\nDA:56,0\nDA:57,0\nDA:59,0\nDA:64,0\nDA:65,0\nDA:67,0\nDA:68,0\nDA:69,0\nDA:70,0\nDA:71,0\nDA:72,0\nDA:73,0\nDA:77,0\nDA:78,0\nDA:79,0\nDA:81,0\nDA:82,0\nDA:85,0\nDA:86,0\nDA:90,0\nDA:91,0\nDA:92,0\nDA:93,0\nDA:95,0\nDA:98,0\nDA:99,0\nLF:37\nLH:0\nend_of_record\nSF:lib/data/model/app/bak/backup2.g.dart\nDA:9,0\nDA:10,0\nDA:11,0\nDA:12,0\nDA:13,0\nDA:14,0\nDA:15,0\nDA:16,0\nDA:17,0\nDA:20,0\nDA:21,0\nDA:22,0\nDA:23,0\nDA:24,0\nDA:25,0\nDA:26,0\nDA:27,0\nDA:28,0\nLF:18\nLH:0\nend_of_record\nSF:lib/data/model/app/bak/utils.dart\nDA:6,0\nDA:8,0\nDA:9,0\nDA:11,0\nDA:12,0\nLF:5\nLH:0\nend_of_record\nSF:lib/core/utils/server.dart\nDA:15,0\nDA:16,0\nDA:20,0\nDA:22,0\nDA:23,0\nDA:24,0\nDA:29,0\nDA:30,0\nDA:32,0\nDA:34,0\nDA:37,0\nDA:56,0\nDA:60,0\nDA:62,0\nDA:66,0\nDA:67,0\nDA:69,0\nDA:71,0\nDA:76,0\nDA:78,0\nDA:79,0\nDA:81,0\nDA:83,0\nDA:85,0\nDA:89,0\nDA:91,0\nDA:93,0\nDA:94,0\nDA:96,0\nDA:97,0\nDA:103,0\nDA:105,0\nDA:106,0\nDA:108,0\nDA:110,0\nLF:35\nLH:0\nend_of_record\nSF:lib/data/model/app/error.dart\nDA:7,0\nDA:9,0\nDA:10,0\nDA:11,0\nDA:12,0\nDA:13,0\nDA:14,0\nDA:32,0\nDA:34,0\nDA:41,0\nDA:43,0\nDA:50,0\nDA:52,0\nDA:59,0\nDA:61,0\nLF:15\nLH:0\nend_of_record\nSF:lib/data/model/server/server_private_info.dart\nDA:24,0\nDA:53,0\nDA:55,0\nDA:56,0\nDA:58,0\nDA:59,0\nDA:67,0\nDA:70,0\nDA:77,0\nDA:78,0\nDA:79,0\nDA:80,0\nDA:81,0\nDA:82,0\nDA:85,0\nDA:87,0\nDA:88,0\nDA:90,0\nDA:91,0\nDA:92,0\nDA:93,0\nDA:94,0\nDA:95,0\nDA:96,0\nDA:97,0\nDA:98,0\nDA:101,0\nDA:102,0\nDA:103,0\nDA:105,0\nDA:106,0\nDA:107,0\nDA:109,0\nDA:110,0\nDA:111,0\nDA:112,0\nDA:114,0\nDA:115,0\nDA:116,0\nDA:117,0\nDA:125,0\nDA:132,0\nDA:136,0\nDA:139,0\nDA:146,0\nLF:45\nLH:0\nend_of_record\nSF:lib/data/model/server/server_private_info.g.dart\nDA:9,0\nDA:10,0\nDA:11,0\nDA:12,0\nDA:13,0\nDA:14,0\nDA:15,0\nDA:16,0\nDA:17,0\nDA:18,0\nDA:19,0\nDA:20,0\nDA:22,0\nDA:23,0\nDA:25,0\nDA:26,0\nDA:27,0\nDA:29,0\nDA:30,0\nDA:32,0\nDA:36,0\nDA:37,0\nDA:38,0\nDA:39,0\nDA:40,0\nDA:41,0\nDA:42,0\nDA:43,0\nDA:44,0\nDA:45,0\nDA:46,0\nDA:47,0\nDA:48,0\nDA:49,0\nDA:50,0\nDA:51,0\nDA:52,0\nLF:37\nLH:0\nend_of_record\nSF:lib/data/res/store.dart\nDA:10,0\nDA:11,0\nDA:12,0\nDA:13,0\nDA:14,0\nDA:15,0\nDA:18,0\nDA:19,0\nDA:20,0\nDA:21,0\nDA:22,0\nDA:23,0\nDA:24,0\nDA:27,0\nDA:28,0\nDA:31,0\nDA:33,0\nDA:34,0\nDA:39,0\nDA:40,0\nDA:41,0\nDA:45,0\nLF:22\nLH:0\nend_of_record\nSF:lib/core/utils/ssh_auth.dart\nDA:9,0\nDA:14,0\nDA:15,0\nDA:16,0\nDA:17,0\nDA:19,0\nLF:6\nLH:0\nend_of_record\nSF:lib/data/provider/app.dart\nDA:17,0\nDA:22,0\nDA:23,0\nLF:3\nLH:0\nend_of_record\nSF:lib/data/provider/app.g.dart\nDA:9,0\nDA:13,0\nLF:2\nLH:0\nend_of_record\nSF:lib/data/helper/system_detector.dart\nDA:16,0\nDA:21,0\nDA:23,0\nDA:29,0\nDA:30,0\nDA:31,0\nDA:33,0\nDA:38,0\nDA:39,0\nDA:41,0\nDA:43,0\nDA:45,0\nDA:49,0\nDA:54,0\nLF:14\nLH:0\nend_of_record\nSF:lib/data/model/app/bak/backup.dart\nDA:17,0\nDA:32,0\nDA:44,0\nDA:46,0\nDA:48,0\nDA:49,0\nDA:50,0\nDA:52,0\nDA:53,0\nDA:54,0\nDA:55,0\nDA:56,0\nDA:58,0\nDA:59,0\nDA:63,0\nDA:64,0\nDA:65,0\nDA:66,0\nDA:67,0\nDA:71,0\nDA:73,0\nDA:74,0\nDA:75,0\nDA:77,0\nDA:83,0\nDA:84,0\nDA:87,0\nDA:88,0\nDA:89,0\nDA:90,0\nDA:91,0\nDA:92,0\nDA:93,0\nDA:95,0\nDA:96,0\nDA:98,0\nDA:99,0\nDA:105,0\nDA:106,0\nDA:109,0\nDA:110,0\nDA:111,0\nDA:112,0\nDA:113,0\nDA:114,0\nDA:115,0\nDA:117,0\nDA:118,0\nDA:120,0\nDA:121,0\nDA:127,0\nDA:128,0\nDA:131,0\nDA:132,0\nDA:133,0\nDA:134,0\nDA:135,0\nDA:136,0\nDA:137,0\nDA:139,0\nDA:140,0\nDA:142,0\nDA:143,0\nDA:149,0\nDA:151,0\nDA:152,0\nDA:153,0\nDA:154,0\nDA:155,0\nDA:156,0\nDA:157,0\nDA:159,0\nDA:160,0\nDA:162,0\nDA:163,0\nDA:169,0\nDA:171,0\nDA:172,0\nDA:173,0\nDA:174,0\nDA:175,0\nDA:176,0\nDA:177,0\nDA:179,0\nDA:180,0\nDA:182,0\nDA:183,0\nDA:188,0\nDA:191,0\nDA:193,0\nDA:194,0\nDA:195,0\nDA:196,0\nDA:197,0\nDA:198,0\nDA:199,0\nDA:201,0\nDA:202,0\nDA:204,0\nDA:205,0\nDA:210,0\nDA:211,0\nDA:213,0\nDA:216,0\nDA:219,0\nDA:221,0\nDA:223,0\nDA:224,0\nDA:225,0\nDA:226,0\nDA:228,0\nDA:230,0\nLF:112\nLH:0\nend_of_record\nSF:lib/data/model/app/bak/backup.g.dart\nDA:9,0\nDA:10,0\nDA:11,0\nDA:12,0\nDA:13,0\nDA:14,0\nDA:15,0\nDA:16,0\nDA:17,0\nDA:18,0\nDA:19,0\nDA:20,0\nDA:21,0\nDA:22,0\nDA:23,0\nDA:24,0\nDA:27,0\nDA:28,0\nDA:29,0\nDA:30,0\nDA:31,0\nDA:32,0\nDA:33,0\nDA:34,0\nDA:35,0\nDA:36,0\nLF:26\nLH:0\nend_of_record\nSF:lib/data/model/server/private_key_info.dart\nDA:11,0\nDA:13,0\nDA:15,0\nDA:17,0\nDA:18,0\nDA:19,0\nDA:22,0\nDA:23,0\nDA:24,0\nDA:27,0\nLF:10\nLH:0\nend_of_record\nSF:lib/data/model/server/private_key_info.g.dart\nDA:9,0\nDA:10,0\nDA:11,0\nDA:12,0\nDA:15,0\nDA:16,0\nLF:6\nLH:0\nend_of_record\nSF:lib/data/model/server/snippet.dart\nDA:23,0\nDA:35,0\nDA:37,0\nDA:38,0\nDA:39,0\nDA:40,0\nDA:41,0\nDA:47,0\nDA:48,0\nDA:49,0\nDA:52,0\nDA:53,0\nDA:54,0\nDA:59,0\nDA:60,0\nDA:61,0\nDA:66,0\nDA:67,0\nDA:68,0\nDA:69,0\nDA:70,0\nDA:75,0\nDA:76,0\nDA:80,0\nDA:81,0\nDA:82,0\nDA:83,0\nDA:86,0\nDA:88,0\nDA:89,0\nDA:92,0\nDA:94,0\nDA:97,0\nDA:102,0\nDA:103,0\nDA:108,0\nDA:109,0\nDA:112,0\nDA:115,0\nDA:121,0\nDA:122,0\nDA:123,0\nDA:127,0\nDA:130,0\nDA:131,0\nDA:132,0\nDA:134,0\nDA:137,0\nDA:140,0\nDA:141,0\nDA:144,0\nDA:145,0\nDA:146,0\nDA:147,0\nDA:148,0\nDA:149,0\nDA:150,0\nDA:154,0\nDA:162,0\nDA:168,0\nDA:176,0\nDA:177,0\nDA:179,0\nDA:180,0\nDA:183,0\nDA:184,0\nDA:185,0\nDA:186,0\nLF:68\nLH:0\nend_of_record\nSF:lib/data/model/server/snippet.g.dart\nDA:9,0\nDA:10,0\nDA:11,0\nDA:12,0\nDA:13,0\nDA:14,0\nDA:15,0\nDA:16,0\nDA:19,0\nDA:20,0\nDA:21,0\nDA:22,0\nDA:23,0\nDA:24,0\nLF:14\nLH:0\nend_of_record\nSF:lib/data/model/app/menu/server_func.dart\nDA:21,0\nDA:22,0\nDA:23,0\nDA:24,0\nDA:25,0\nDA:26,0\nDA:27,0\nDA:32,0\nDA:40,0\nDA:42,0\nDA:43,0\nDA:44,0\nDA:46,0\nDA:47,0\nDA:48,0\nDA:49,0\nDA:50,0\nDA:53,0\nDA:54,0\nDA:55,0\nDA:57,0\nDA:58,0\nDA:59,0\nDA:60,0\nDA:61,0\nLF:25\nLH:0\nend_of_record\nSF:lib/data/model/app/net_view.dart\nDA:10,0\nDA:11,0\nDA:12,0\nDA:13,0\nDA:16,0\nDA:17,0\nDA:18,0\nDA:19,0\nDA:24,0\nDA:25,0\nDA:28,0\nDA:29,0\nDA:30,0\nDA:32,0\nDA:34,0\nDA:35,0\nDA:37,0\nDA:39,0\nDA:42,0\nDA:47,0\nDA:48,0\nDA:49,0\nDA:50,0\nDA:53,0\nDA:54,0\nDA:55,0\nLF:26\nLH:0\nend_of_record\nSF:lib/data/model/server/server.dart\nDA:24,0\nDA:26,0\nDA:28,0\nDA:30,0\nDA:53,1\nDA:84,0\nLF:6\nLH:1\nend_of_record\nSF:lib/data/model/app/script_builders.dart\nDA:6,6\nDA:29,6\nDA:31,1\nDA:34,1\nDA:36,1\nDA:41,1\nDA:43,2\nDA:46,1\nDA:51,2\nDA:52,6\nDA:57,1\nDA:59,1\nDA:60,1\nDA:69,2\nDA:70,1\nDA:72,1\nDA:73,1\nDA:74,7\nDA:77,1\nDA:81,1\nDA:84,2\nDA:85,1\nDA:86,2\nDA:87,1\nDA:89,1\nDA:93,1\nDA:96,1\nDA:97,5\nDA:98,1\nDA:99,1\nDA:100,1\nDA:101,1\nDA:108,6\nDA:110,0\nDA:113,0\nDA:119,0\nDA:122,0\nDA:124,0\nDA:127,0\nDA:132,0\nDA:133,0\nDA:138,0\nDA:140,0\nDA:141,0\nDA:161,0\nDA:162,0\nDA:163,0\nDA:164,0\nDA:165,0\nDA:169,0\nDA:173,0\nDA:174,0\nDA:175,0\nDA:176,0\nDA:177,0\nDA:179,0\nDA:181,0\nDA:186,0\nDA:189,0\nDA:191,0\nDA:194,0\nDA:196,0\nDA:197,0\nDA:198,0\nDA:210,0\nDA:217,0\nDA:224,0\nDA:237,0\nDA:239,1\nLF:69\nLH:34\nend_of_record\nSF:lib/data/model/app/server_detail_card.dart\nDA:28,0\nDA:29,0\nDA:31,0\nDA:33,0\nDA:34,0\nDA:35,0\nDA:36,0\nDA:37,0\nDA:38,0\nDA:39,0\nDA:40,0\nDA:41,0\nDA:42,0\nDA:43,0\nDA:44,0\nDA:45,0\nDA:46,0\nDA:52,0\nDA:53,0\nDA:54,0\nDA:55,0\nDA:56,0\nDA:57,0\nDA:58,0\nDA:62,0\nDA:63,0\nDA:64,0\nDA:65,0\nDA:66,0\nDA:67,0\nLF:30\nLH:0\nend_of_record\nSF:lib/data/provider/server.dart\nDA:24,6\nDA:27,3\nDA:28,0\nDA:29,0\nDA:30,0\nDA:34,0\nDA:36,0\nDA:38,0\nDA:40,0\nDA:43,0\nDA:44,0\nDA:45,0\nDA:47,0\nDA:48,0\nDA:49,0\nDA:50,0\nDA:54,0\nDA:55,0\nDA:56,0\nDA:58,0\nDA:59,0\nDA:62,0\nDA:63,0\nDA:64,0\nDA:65,0\nDA:67,0\nDA:70,0\nDA:71,0\nDA:73,0\nDA:75,0\nDA:81,1\nDA:83,0\nDA:86,2\nDA:91,0\nDA:93,0\nDA:94,0\nDA:96,0\nDA:97,0\nDA:100,0\nDA:103,0\nDA:104,0\nDA:109,0\nDA:111,0\nDA:112,0\nDA:116,0\nDA:117,0\nDA:118,0\nDA:120,0\nDA:121,0\nDA:124,0\nDA:126,0\nDA:131,0\nDA:143,0\nDA:144,0\nDA:149,0\nDA:155,0\nDA:156,0\nDA:159,0\nDA:160,0\nDA:161,0\nDA:162,0\nDA:163,0\nDA:165,0\nDA:167,0\nDA:168,0\nDA:172,0\nDA:174,0\nDA:179,0\nDA:181,0\nDA:182,0\nDA:183,0\nDA:184,0\nDA:189,0\nDA:191,0\nDA:192,0\nDA:196,0\nDA:199,0\nDA:200,0\nDA:202,0\nDA:205,0\nDA:206,0\nDA:207,0\nDA:208,0\nDA:209,0\nDA:210,0\nDA:213,0\nDA:214,0\nDA:215,0\nDA:216,0\nDA:217,0\nDA:218,0\nDA:219,0\nDA:220,0\nDA:221,0\nDA:224,0\nDA:225,0\nDA:226,0\nDA:227,0\nDA:228,0\nDA:229,0\nDA:230,0\nDA:231,0\nDA:234,0\nDA:235,0\nDA:236,0\nDA:237,0\nDA:238,0\nDA:239,0\nDA:240,0\nDA:241,0\nDA:244,0\nDA:245,0\nDA:246,0\nDA:247,0\nDA:249,0\nDA:250,0\nDA:251,0\nDA:252,0\nDA:253,0\nDA:254,0\nDA:258,0\nDA:260,0\nDA:261,0\nDA:264,0\nDA:265,0\nDA:268,0\nDA:269,0\nDA:270,0\nDA:273,0\nDA:274,0\nDA:275,0\nDA:279,0\nDA:280,0\nDA:281,0\nDA:282,0\nDA:287,0\nDA:289,0\nDA:290,0\nDA:292,0\nDA:295,0\nDA:303,0\nDA:309,0\nDA:310,0\nDA:312,0\nDA:313,0\nDA:315,0\nDA:316,0\nDA:317,0\nDA:318,0\nDA:320,0\nDA:323,0\nDA:324,0\nDA:325,0\nDA:328,0\nDA:332,0\nDA:336,0\nDA:337,0\nDA:339,0\nDA:340,0\nDA:341,0\nDA:342,0\nDA:343,0\nDA:344,0\nDA:345,0\nDA:348,0\nDA:349,0\nDA:350,0\nDA:351,0\nDA:352,0\nDA:353,0\nDA:355,0\nDA:356,0\nDA:357,0\nDA:358,0\nDA:359,0\nDA:360,0\nDA:365,0\nDA:366,0\nDA:367,0\nDA:368,0\nDA:372,0\nDA:377,0\nDA:378,0\nDA:385,0\nDA:386,0\nDA:387,0\nDA:388,0\nDA:389,0\nDA:391,0\nDA:395,0\nDA:396,0\nDA:397,0\nDA:401,0\nDA:402,0\nDA:403,0\nDA:404,0\nDA:408,0\nDA:409,0\nDA:410,0\nDA:411,0\nDA:412,0\nDA:413,0\nDA:414,0\nDA:417,0\nDA:418,0\nDA:419,0\nDA:421,0\nDA:423,0\nDA:426,0\nDA:429,0\nDA:430,0\nDA:433,0\nDA:435,0\nDA:437,0\nDA:438,0\nDA:439,0\nDA:440,0\nDA:445,0\nDA:447,0\nLF:219\nLH:4\nend_of_record\nSF:lib/data/model/server/amd.dart\nDA:31,2\nDA:33,2\nDA:34,2\nDA:37,3\nDA:38,2\nDA:39,1\nDA:40,1\nDA:42,2\nDA:46,1\nDA:48,3\nDA:49,4\nDA:52,3\nDA:53,1\nDA:56,2\nDA:57,3\nDA:58,1\nDA:61,4\nDA:64,4\nDA:67,3\nDA:70,4\nDA:72,1\nDA:87,1\nDA:89,1\nDA:90,1\nDA:92,2\nDA:93,1\nDA:98,1\nDA:99,1\nDA:100,1\nDA:102,2\nDA:103,2\nDA:104,1\nDA:107,1\nDA:108,3\nDA:109,3\nDA:110,2\nDA:112,1\nDA:113,1\nDA:114,1\nDA:115,2\nDA:116,1\nDA:117,1\nDA:118,1\nDA:123,1\nDA:126,1\nDA:127,2\nDA:128,4\nDA:129,3\nDA:131,1\nDA:132,1\nDA:146,1\nDA:157,1\nDA:159,6\nDA:169,1\nDA:171,1\nDA:173,6\nDA:182,1\nDA:184,1\nDA:186,4\nLF:59\nLH:59\nend_of_record\nSF:lib/data/model/server/battery.dart\nDA:22,2\nDA:24,1\nDA:25,1\nDA:26,1\nDA:27,2\nDA:28,1\nDA:29,2\nDA:30,3\nDA:33,1\nDA:34,1\nDA:36,1\nDA:37,1\nDA:39,1\nDA:40,1\nDA:41,2\nDA:43,1\nDA:44,1\nDA:48,0\nDA:50,0\nDA:53,0\nDA:62,1\nDA:64,1\nDA:66,1\nDA:68,1\nDA:77,1\nDA:78,1\nDA:79,1\nDA:80,1\nDA:81,2\nDA:82,1\nDA:84,2\nDA:85,0\nDA:86,1\nDA:88,0\nDA:90,1\nDA:92,1\nLF:36\nLH:31\nend_of_record\nSF:lib/data/model/server/conn.dart\nDA:9,7\nDA:11,0\nDA:12,0\nDA:13,0\nDA:14,0\nDA:15,0\nDA:16,0\nDA:17,0\nDA:18,0\nDA:19,0\nDA:20,0\nLF:11\nLH:1\nend_of_record\nSF:lib/data/model/server/cpu.dart\nDA:10,1\nDA:14,1\nDA:16,3\nDA:17,8\nDA:18,2\nDA:19,2\nDA:20,2\nDA:21,2\nDA:22,1\nDA:26,1\nDA:27,5\nDA:28,2\nDA:30,7\nDA:31,7\nDA:32,1\nDA:33,3\nDA:35,0\nDA:41,0\nDA:44,2\nDA:47,0\nDA:48,1\nDA:49,5\nDA:50,7\nDA:51,2\nDA:52,2\nDA:56,0\nDA:57,1\nDA:58,5\nDA:59,7\nDA:60,2\nDA:61,2\nDA:65,0\nDA:66,1\nDA:67,5\nDA:68,7\nDA:69,2\nDA:70,2\nDA:74,0\nDA:75,3\nDA:77,1\nDA:86,1\nDA:92,0\nDA:93,1\nDA:94,2\nDA:95,3\nDA:96,3\nDA:98,2\nDA:99,4\nDA:100,1\nDA:145,1\nDA:156,14\nDA:158,0\nDA:159,0\nDA:161,0\nDA:162,0\nDA:164,0\nDA:165,0\nDA:166,0\nDA:168,0\nDA:169,0\nDA:170,0\nDA:172,0\nDA:173,0\nDA:174,0\nDA:175,0\nDA:176,0\nDA:177,0\nDA:178,0\nDA:187,0\nDA:188,0\nDA:190,0\nDA:191,0\nDA:192,0\nDA:193,0\nDA:194,0\nDA:195,0\nDA:202,0\nDA:203,0\nDA:205,0\nDA:215,0\nDA:216,0\nDA:219,0\nDA:221,0\nDA:222,0\nDA:223,0\nDA:225,0\nDA:226,0\nDA:241,0\nDA:243,0\nDA:244,0\nDA:245,0\nDA:246,0\nDA:247,0\nDA:249,0\nDA:250,0\nDA:265,0\nDA:266,0\nDA:267,0\nDA:268,0\nDA:270,0\nDA:272,0\nDA:273,0\nDA:274,0\nDA:277,0\nDA:278,0\nDA:280,0\nDA:281,0\nDA:283,0\nDA:290,0\nDA:291,0\nDA:293,0\nLF:111\nLH:44\nend_of_record\nSF:lib/data/model/server/custom.dart\nDA:27,0\nDA:38,0\nDA:40,0\nDA:42,0\nDA:44,0\nDA:46,0\nDA:47,0\nDA:48,0\nDA:49,0\nDA:50,0\nDA:51,0\nDA:52,0\nDA:55,0\nDA:58,0\nDA:59,0\nDA:60,0\nDA:61,0\nDA:62,0\nDA:63,0\nDA:64,0\nLF:20\nLH:0\nend_of_record\nSF:lib/data/model/server/custom.g.dart\nDA:9,0\nDA:10,0\nDA:11,0\nDA:12,0\nDA:13,0\nDA:15,0\nDA:16,0\nDA:17,0\nDA:18,0\nDA:21,0\nDA:22,0\nDA:23,0\nDA:24,0\nDA:25,0\nDA:26,0\nDA:27,0\nDA:28,0\nDA:29,0\nLF:18\nLH:0\nend_of_record\nSF:lib/data/model/server/disk_smart.dart\nDA:11,1\nDA:25,0\nDA:27,1\nDA:28,1\nDA:30,5\nDA:32,2\nDA:34,2\nDA:37,3\nDA:39,1\nDA:41,1\nDA:45,1\nDA:46,1\nDA:47,2\nDA:48,3\nDA:51,1\nDA:52,1\nDA:54,2\nDA:56,1\nDA:58,1\nDA:59,1\nDA:72,2\nDA:78,1\nDA:79,1\nDA:82,1\nDA:83,1\nDA:84,1\nDA:85,1\nDA:86,1\nDA:87,1\nDA:88,1\nDA:91,3\nDA:94,1\nDA:96,1\nDA:97,1\nDA:98,0\nDA:99,0\nDA:103,1\nDA:104,0\nDA:106,0\nDA:107,0\nDA:112,1\nDA:114,0\nDA:115,0\nDA:119,2\nDA:123,2\nDA:124,1\nDA:125,2\nDA:126,1\nDA:132,2\nDA:133,1\nDA:134,1\nDA:136,1\nDA:145,0\nDA:156,1\nDA:164,1\nDA:165,1\nDA:167,2\nDA:170,2\nDA:171,1\nDA:172,2\nDA:174,2\nDA:175,1\nDA:177,1\nDA:178,1\nDA:179,1\nDA:180,2\nDA:181,2\nDA:182,3\nDA:183,2\nDA:192,0\nDA:195,1\nDA:197,2\nDA:198,2\nDA:201,0\nDA:204,0\nDA:206,0\nDA:208,0\nDA:213,0\nDA:214,0\nDA:222,3\nDA:224,4\nDA:225,4\nDA:226,4\nDA:227,4\nDA:228,4\nDA:229,4\nDA:231,0\nDA:232,0\nDA:237,1\nDA:251,0\nDA:253,0\nDA:255,0\nDA:261,1\nDA:274,0\nDA:276,1\nDA:277,1\nDA:278,1\nDA:279,2\nDA:280,2\nDA:281,2\nDA:282,2\nDA:283,2\nDA:284,2\nDA:285,2\nDA:289,0\nDA:291,0\nLF:106\nLH:82\nend_of_record\nSF:lib/data/model/server/disk_smart.g.dart\nDA:9,0\nDA:10,0\nDA:11,0\nDA:12,0\nDA:13,0\nDA:14,0\nDA:15,0\nDA:16,0\nDA:17,0\nDA:18,0\nDA:19,0\nDA:23,1\nDA:24,1\nDA:25,1\nDA:26,1\nDA:27,1\nDA:28,1\nDA:29,1\nDA:30,1\nDA:31,1\nDA:32,1\nDA:33,1\nDA:36,0\nDA:37,0\nDA:38,0\nDA:39,0\nDA:40,0\nDA:41,0\nDA:42,0\nDA:43,0\nDA:44,0\nDA:45,0\nDA:46,0\nDA:47,0\nDA:51,0\nDA:52,0\nDA:53,0\nDA:54,0\nDA:55,0\nDA:56,0\nDA:57,0\nDA:58,0\nDA:59,0\nDA:60,0\nDA:61,0\nDA:64,0\nDA:65,0\nDA:66,0\nDA:67,0\nDA:68,0\nDA:69,0\nDA:70,0\nDA:71,0\nDA:72,0\nDA:73,0\nDA:76,0\nDA:78,0\nDA:79,0\nDA:80,0\nDA:81,0\nDA:82,0\nDA:83,0\nDA:84,0\nDA:85,0\nDA:86,0\nLF:65\nLH:11\nend_of_record\nSF:lib/data/model/server/memory.dart\nDA:8,7\nDA:10,0\nDA:11,0\nDA:12,0\nDA:14,0\nDA:17,0\nDA:19,0\nDA:20,0\nDA:22,0\nDA:23,0\nDA:24,0\nDA:25,0\nDA:26,0\nDA:27,0\nDA:28,0\nDA:29,0\nDA:30,0\nDA:32,0\nDA:36,0\nDA:43,0\nDA:45,0\nDA:47,0\nDA:50,0\nDA:51,0\nDA:52,0\nDA:53,0\nDA:55,0\nDA:56,0\nDA:57,0\nDA:61,0\nDA:63,0\nDA:65,0\nDA:68,0\nDA:69,0\nDA:70,0\nDA:71,0\nDA:72,0\nDA:75,0\nDA:76,0\nDA:77,0\nDA:78,0\nDA:81,0\nDA:85,0\nDA:89,0\nDA:90,0\nDA:91,0\nDA:92,0\nDA:93,0\nDA:94,0\nDA:95,0\nDA:96,0\nDA:97,0\nDA:98,0\nDA:99,0\nDA:101,0\nDA:110,6\nDA:112,0\nDA:114,0\nDA:116,0\nDA:118,0\nDA:121,0\nDA:122,0\nDA:124,0\nDA:125,0\nDA:126,0\nDA:127,0\nDA:128,0\nDA:129,0\nDA:130,0\nDA:131,0\nDA:132,0\nDA:134,0\nLF:72\nLH:2\nend_of_record\nSF:lib/data/model/server/net_speed.dart\nDA:13,1\nDA:15,0\nDA:16,0\nDA:22,1\nDA:24,0\nDA:26,0\nDA:27,0\nDA:29,0\nDA:30,0\nDA:32,0\nDA:33,0\nDA:34,0\nDA:35,0\nDA:37,0\nDA:53,0\nDA:55,0\nDA:56,0\nDA:57,0\nDA:58,0\nDA:60,0\nDA:61,0\nDA:62,0\nDA:65,0\nDA:66,0\nDA:67,0\nDA:68,0\nDA:72,0\nDA:74,0\nDA:75,0\nDA:78,0\nDA:79,0\nDA:80,0\nDA:82,0\nDA:83,0\nDA:84,0\nDA:85,0\nDA:86,0\nDA:90,0\nDA:92,0\nDA:93,0\nDA:96,0\nDA:97,0\nDA:98,0\nDA:101,0\nDA:102,0\nDA:103,0\nDA:104,0\nDA:108,0\nDA:110,0\nDA:111,0\nDA:114,0\nDA:115,0\nDA:116,0\nDA:118,0\nDA:119,0\nDA:120,0\nDA:121,0\nDA:122,0\nDA:126,0\nDA:128,0\nDA:129,0\nDA:132,0\nDA:134,0\nDA:135,0\nDA:136,0\nDA:143,0\nDA:150,0\nDA:151,0\nDA:152,0\nDA:153,0\nDA:156,0\nDA:157,0\nDA:159,0\nDA:160,0\nDA:162,0\nDA:163,0\nDA:164,0\nDA:165,0\nDA:166,0\nDA:196,0\nDA:197,0\nDA:198,0\nDA:199,0\nDA:202,0\nDA:203,0\nDA:204,0\nDA:205,0\nDA:206,0\nDA:209,0\nDA:212,0\nDA:215,0\nDA:216,0\nDA:217,0\nLF:93\nLH:2\nend_of_record\nSF:lib/data/model/server/nvdia.dart\nDA:25,2\nDA:26,2\nDA:27,1\nDA:28,3\nDA:29,1\nDA:30,3\nDA:32,1\nDA:33,1\nDA:34,1\nDA:35,1\nDA:36,1\nDA:37,2\nDA:38,3\nDA:39,3\nDA:40,2\nDA:41,3\nDA:42,3\nDA:43,3\nDA:44,3\nDA:45,1\nDA:46,3\nDA:47,3\nDA:48,3\nDA:50,1\nDA:51,1\nDA:53,3\nDA:58,2\nDA:60,1\nDA:61,1\nDA:62,1\nDA:63,1\nDA:64,1\nDA:65,3\nDA:67,1\nDA:69,3\nDA:70,3\nDA:71,3\nDA:72,1\nDA:73,1\nDA:74,3\nDA:75,3\nDA:77,1\nDA:79,3\nDA:84,2\nDA:85,1\nDA:98,1\nDA:108,0\nDA:110,0\nDA:120,1\nDA:122,0\nDA:124,0\nDA:133,1\nDA:135,0\nDA:137,0\nLF:54\nLH:48\nend_of_record\nSF:lib/data/model/server/sensors.dart\nDA:6,6\nDA:17,1\nDA:18,1\nDA:19,1\nDA:20,1\nDA:21,1\nDA:22,0\nDA:31,1\nDA:33,0\nDA:34,0\nDA:35,0\nDA:36,0\nDA:37,0\nDA:38,0\nDA:40,0\nDA:43,1\nDA:44,3\nDA:47,1\nDA:48,2\nDA:49,1\nDA:51,3\nDA:52,1\nDA:53,1\nDA:54,2\nDA:55,1\nDA:58,2\nDA:61,3\nDA:62,0\nDA:65,1\nDA:66,2\nDA:68,1\nDA:69,1\nDA:70,1\nDA:71,5\nDA:73,1\nDA:74,2\nDA:75,1\nDA:76,1\nDA:77,2\nDA:78,2\nDA:79,2\nDA:80,1\nDA:82,2\nLF:43\nLH:34\nend_of_record\nSF:lib/data/model/server/temp.dart\nDA:4,0\nDA:5,0\nDA:6,0\nDA:7,0\nDA:8,0\nDA:9,0\nDA:10,0\nDA:13,0\nDA:14,0\nDA:18,0\nDA:22,0\nDA:23,0\nDA:26,0\nDA:27,0\nDA:30,1\nDA:31,2\nDA:34,0\nDA:35,0\nDA:38,0\nDA:39,0\nDA:40,0\nDA:43,0\nLF:22\nLH:2\nend_of_record\nSF:lib/data/model/server/wol_cfg.dart\nDA:14,0\nDA:16,0\nDA:17,0\nDA:18,0\nDA:19,0\nDA:20,0\nDA:22,0\nDA:29,0\nDA:30,0\nDA:31,0\nDA:34,0\nDA:35,0\nDA:36,0\nDA:37,0\nDA:38,0\nDA:41,0\nDA:43,0\nLF:17\nLH:0\nend_of_record\nSF:lib/data/model/server/wol_cfg.g.dart\nDA:9,0\nDA:10,0\nDA:11,0\nDA:12,0\nDA:15,0\nDA:16,0\nDA:17,0\nDA:18,0\nDA:19,0\nLF:9\nLH:0\nend_of_record\nSF:lib/data/store/server.dart\nDA:9,0\nDA:11,0\nDA:13,0\nDA:14,0\nDA:17,0\nDA:18,0\nDA:19,0\nDA:20,0\nDA:22,0\nDA:23,0\nDA:24,0\nDA:25,0\nDA:28,0\nDA:29,0\nDA:32,0\nDA:39,0\nDA:45,0\nDA:46,0\nDA:49,0\nDA:50,0\nDA:51,0\nDA:53,0\nDA:54,0\nDA:57,0\nDA:59,0\nDA:60,0\nDA:61,0\nDA:64,0\nDA:65,0\nDA:69,0\nDA:72,0\nDA:73,0\nDA:74,0\nDA:78,0\nDA:79,0\nDA:80,0\nDA:83,0\nDA:84,0\nDA:85,0\nDA:90,0\nDA:92,0\nDA:94,0\nDA:95,0\nDA:96,0\nDA:97,0\nDA:98,0\nDA:104,0\nDA:105,0\nDA:106,0\nDA:107,0\nDA:108,0\nDA:109,0\nDA:110,0\nDA:111,0\nDA:116,0\nDA:118,0\nDA:119,0\nDA:124,0\nLF:58\nLH:0\nend_of_record\nSF:lib/data/model/server/windows_parser.dart\nDA:16,0\nDA:19,1\nDA:26,2\nDA:27,0\nDA:29,0\nDA:30,0\nDA:31,0\nDA:32,0\nDA:34,0\nDA:36,0\nDA:41,0\nDA:46,1\nDA:49,2\nDA:50,4\nDA:51,1\nDA:53,1\nDA:54,2\nDA:59,1\nDA:60,1\nDA:61,1\nDA:62,1\nDA:63,1\nDA:64,1\nDA:65,1\nDA:66,1\nDA:67,1\nDA:71,2\nDA:72,1\nDA:77,3\nDA:81,1\nDA:82,1\nDA:85,3\nDA:86,0\nDA:90,1\nDA:91,2\nDA:92,2\nDA:94,1\nDA:95,3\nDA:97,0\nDA:100,0\nDA:106,1\nDA:108,1\nDA:109,1\nDA:111,1\nDA:112,0\nDA:113,0\nDA:114,0\nDA:116,0\nDA:119,0\nDA:120,0\nDA:130,0\nDA:131,0\nDA:133,0\nDA:134,0\nDA:135,0\nDA:146,1\nDA:148,1\nDA:150,1\nDA:153,2\nDA:154,2\nDA:157,2\nDA:158,2\nDA:160,1\nDA:161,1\nDA:176,1\nDA:184,1\nDA:186,1\nDA:187,1\nDA:190,1\nDA:191,1\nDA:193,1\nDA:204,1\nDA:206,1\nDA:207,1\nDA:209,2\nDA:211,2\nDA:212,2\nDA:214,3\nDA:216,3\nDA:217,0\nDA:218,2\nDA:221,1\nDA:222,2\nDA:223,2\nDA:224,1\nDA:227,0\nDA:232,2\nDA:233,2\nDA:234,1\nDA:235,2\nDA:236,4\nDA:239,1\nDA:240,1\nDA:254,3\nDA:255,1\nLF:95\nLH:70\nend_of_record\nSF:lib/data/model/server/try_limiter.dart\nDA:6,0\nDA:8,0\nDA:9,0\nDA:10,0\nDA:13,0\nDA:14,0\nDA:20,0\nDA:21,0\nDA:24,0\nDA:25,0\nDA:28,0\nDA:29,0\nLF:12\nLH:0\nend_of_record\nSF:lib/data/model/ssh/virtual_key.dart\nDA:59,0\nDA:60,0\nDA:61,0\nDA:62,0\nDA:63,0\nDA:64,0\nDA:65,0\nDA:66,0\nDA:67,0\nDA:68,0\nDA:69,0\nDA:70,0\nDA:71,0\nDA:72,0\nDA:73,0\nDA:74,0\nDA:75,0\nDA:80,0\nDA:81,0\nDA:84,0\nDA:85,0\nDA:87,0\nDA:88,0\nDA:90,0\nDA:113,0\nDA:114,0\nDA:115,0\nDA:116,0\nDA:117,0\nDA:118,0\nDA:119,0\nDA:120,0\nDA:121,0\nDA:122,0\nDA:123,0\nDA:124,0\nDA:125,0\nDA:126,0\nDA:127,0\nDA:128,0\nDA:129,0\nDA:130,0\nDA:131,0\nDA:132,0\nDA:133,0\nDA:134,0\nDA:135,0\nDA:136,0\nDA:137,0\nDA:138,0\nDA:143,0\nDA:144,0\nDA:145,0\nDA:146,0\nDA:147,0\nDA:148,0\nDA:149,0\nDA:150,0\nDA:151,0\nDA:158,0\nDA:159,0\nDA:160,0\nDA:161,0\nDA:162,0\nDA:166,0\nDA:167,0\nDA:171,0\nDA:172,0\nDA:176,0\nDA:177,0\nDA:178,0\nDA:179,0\nDA:184,0\nDA:186,0\nDA:187,0\nDA:188,0\nDA:189,0\nDA:190,0\nDA:192,0\nLF:79\nLH:0\nend_of_record\nSF:lib/data/store/container.dart\nDA:8,0\nDA:10,0\nDA:12,0\nDA:13,0\nDA:16,0\nDA:17,0\nDA:20,0\nDA:21,0\nDA:23,0\nDA:27,0\nDA:30,0\nDA:31,0\nDA:35,0\nDA:36,0\nDA:38,0\nDA:41,0\nLF:16\nLH:0\nend_of_record\nSF:lib/data/store/history.dart\nDA:10,0\nDA:13,0\nDA:15,0\nDA:16,0\nDA:17,0\nDA:18,0\nDA:21,0\nDA:29,0\nDA:32,0\nDA:34,0\nDA:35,0\nDA:36,0\nDA:39,0\nDA:43,0\nDA:45,0\nLF:15\nLH:0\nend_of_record\nSF:lib/data/store/private_key.dart\nDA:6,0\nDA:8,0\nDA:10,0\nDA:11,0\nDA:14,0\nDA:15,0\nDA:16,0\nDA:17,0\nDA:19,0\nDA:20,0\nDA:21,0\nDA:22,0\nDA:25,0\nDA:26,0\nDA:29,0\nDA:36,0\nDA:42,0\nDA:44,0\nDA:47,0\nDA:48,0\nLF:20\nLH:0\nend_of_record\nSF:lib/data/store/setting.dart\nDA:11,0\nDA:13,0\nLF:2\nLH:0\nend_of_record\nSF:lib/data/store/snippet.dart\nDA:6,0\nDA:8,0\nDA:10,0\nDA:11,0\nDA:14,0\nDA:16,0\nDA:17,0\nDA:19,0\nDA:20,0\nDA:21,0\nDA:22,0\nDA:25,0\nDA:26,0\nDA:29,0\nDA:36,0\nDA:39,0\nDA:42,0\nDA:43,0\nDA:46,0\nDA:47,0\nDA:48,0\nDA:50,0\nDA:51,0\nDA:54,0\nLF:24\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n.dart\nDA:75,0\nDA:76,0\nDA:80,0\nDA:81,0\nDA:1550,8\nDA:1552,0\nDA:1554,0\nDA:1557,0\nDA:1558,0\nDA:1571,0\nDA:1573,0\nDA:1577,0\nDA:1579,0\nDA:1580,0\nDA:1582,0\nDA:1583,0\nDA:1584,0\nDA:1591,0\nDA:1592,0\nDA:1593,0\nDA:1594,0\nDA:1595,0\nDA:1596,0\nDA:1597,0\nDA:1598,0\nDA:1599,0\nDA:1600,0\nDA:1601,0\nDA:1602,0\nDA:1603,0\nDA:1604,0\nDA:1605,0\nDA:1606,0\nDA:1607,0\nDA:1608,0\nDA:1609,0\nDA:1610,0\nDA:1611,0\nDA:1612,0\nDA:1613,0\nDA:1614,0\nDA:1615,0\nDA:1618,0\nLF:43\nLH:1\nend_of_record\nSF:lib/generated/l10n/l10n_en.dart\nDA:9,0\nDA:11,0\nDA:15,0\nDA:18,0\nDA:22,0\nDA:25,0\nDA:28,0\nDA:31,0\nDA:35,0\nDA:39,0\nDA:42,0\nDA:45,0\nDA:48,0\nDA:52,0\nDA:55,0\nDA:58,0\nDA:62,0\nDA:65,0\nDA:68,0\nDA:71,0\nDA:74,0\nDA:77,0\nDA:80,0\nDA:83,0\nDA:87,0\nDA:90,0\nDA:93,0\nDA:97,0\nDA:100,0\nDA:103,0\nDA:107,0\nDA:110,0\nDA:113,0\nDA:117,0\nDA:120,0\nDA:123,0\nDA:127,0\nDA:130,0\nDA:133,0\nDA:136,0\nDA:139,0\nDA:143,0\nDA:146,0\nDA:149,0\nDA:152,0\nDA:155,0\nDA:158,0\nDA:161,0\nDA:163,0\nDA:166,0\nDA:170,0\nDA:172,0\nDA:175,0\nDA:178,0\nDA:183,0\nDA:186,0\nDA:188,0\nDA:191,0\nDA:194,0\nDA:198,0\nDA:201,0\nDA:204,0\nDA:208,0\nDA:211,0\nDA:214,0\nDA:217,0\nDA:220,0\nDA:223,0\nDA:226,0\nDA:230,0\nDA:233,0\nDA:237,0\nDA:239,0\nDA:242,0\nDA:245,0\nDA:248,0\nDA:251,0\nDA:254,0\nDA:257,0\nDA:260,0\nDA:263,0\nDA:267,0\nDA:270,0\nDA:273,0\nDA:276,0\nDA:279,0\nDA:282,0\nDA:285,0\nDA:287,0\nDA:290,0\nDA:293,0\nDA:296,0\nDA:299,0\nDA:302,0\nDA:305,0\nDA:308,0\nDA:312,0\nDA:315,0\nDA:318,0\nDA:321,0\nDA:324,0\nDA:328,0\nDA:331,0\nDA:334,0\nDA:338,0\nDA:341,0\nDA:344,0\nDA:347,0\nDA:349,0\nDA:352,0\nDA:355,0\nDA:358,0\nDA:361,0\nDA:364,0\nDA:367,0\nDA:370,0\nDA:373,0\nDA:377,0\nDA:380,0\nDA:384,0\nDA:387,0\nDA:390,0\nDA:393,0\nDA:396,0\nDA:399,0\nDA:402,0\nDA:406,0\nDA:409,0\nDA:412,0\nDA:415,0\nDA:418,0\nDA:421,0\nDA:425,0\nDA:428,0\nDA:432,0\nDA:436,0\nDA:438,0\nDA:441,0\nDA:444,0\nDA:447,0\nDA:450,0\nDA:454,0\nDA:457,0\nDA:460,0\nDA:463,0\nDA:466,0\nDA:469,0\nDA:472,0\nDA:475,0\nDA:478,0\nDA:481,0\nDA:485,0\nDA:489,0\nDA:493,0\nDA:496,0\nDA:499,0\nDA:502,0\nDA:505,0\nDA:508,0\nDA:511,0\nDA:514,0\nDA:517,0\nDA:520,0\nDA:523,0\nDA:526,0\nDA:529,0\nDA:532,0\nDA:535,0\nDA:538,0\nDA:541,0\nDA:544,0\nDA:547,0\nDA:550,0\nDA:553,0\nDA:556,0\nDA:559,0\nDA:562,0\nDA:566,0\nDA:569,0\nDA:572,0\nDA:575,0\nDA:578,0\nDA:581,0\nDA:584,0\nDA:587,0\nDA:590,0\nDA:593,0\nDA:597,0\nDA:600,0\nDA:602,0\nDA:605,0\nDA:609,0\nDA:611,0\nDA:614,0\nDA:617,0\nDA:620,0\nDA:623,0\nDA:626,0\nDA:629,0\nDA:632,0\nDA:635,0\nDA:639,0\nDA:642,0\nDA:646,0\nDA:648,0\nDA:651,0\nDA:654,0\nDA:658,0\nDA:661,0\nDA:664,0\nDA:667,0\nDA:671,0\nDA:674,0\nDA:677,0\nDA:680,0\nDA:684,0\nDA:687,0\nDA:690,0\nDA:693,0\nDA:696,0\nDA:699,0\nDA:702,0\nDA:705,0\nDA:708,0\nDA:711,0\nDA:714,0\nDA:718,0\nDA:721,0\nDA:724,0\nDA:727,0\nDA:730,0\nDA:733,0\nDA:737,0\nDA:740,0\nDA:743,0\nDA:746,0\nDA:749,0\nDA:752,0\nDA:756,0\nDA:759,0\nDA:762,0\nDA:766,0\nDA:769,0\nDA:772,0\nDA:775,0\nDA:778,0\nDA:782,0\nDA:785,0\nDA:789,0\nLF:249\nLH:0\nend_of_record\nSF:lib/data/model/server/pve.dart\nDA:11,2\nDA:12,1\nDA:13,1\nDA:14,1\nDA:15,1\nDA:16,1\nDA:20,0\nDA:21,0\nDA:22,0\nDA:23,0\nDA:24,0\nDA:25,0\nDA:34,1\nDA:35,2\nDA:38,1\nDA:39,1\nDA:40,1\nDA:41,1\nDA:42,1\nDA:43,1\nDA:44,1\nDA:45,1\nDA:46,1\nDA:47,1\nDA:84,1\nDA:104,1\nDA:105,1\nDA:106,1\nDA:108,1\nDA:109,1\nDA:110,1\nDA:111,1\nDA:112,1\nDA:113,1\nDA:114,1\nDA:115,2\nDA:116,1\nDA:117,1\nDA:118,1\nDA:119,1\nDA:120,1\nDA:121,1\nDA:122,1\nDA:126,0\nDA:127,0\nDA:129,0\nDA:131,0\nDA:132,0\nDA:134,0\nDA:162,1\nDA:182,1\nDA:183,1\nDA:184,1\nDA:186,1\nDA:187,1\nDA:188,1\nDA:189,1\nDA:190,1\nDA:191,1\nDA:192,1\nDA:193,2\nDA:194,1\nDA:195,1\nDA:196,1\nDA:197,1\nDA:198,1\nDA:199,1\nDA:200,1\nDA:204,0\nDA:205,0\nDA:207,0\nDA:209,0\nDA:210,0\nDA:212,0\nDA:230,1\nDA:242,1\nDA:243,1\nDA:244,1\nDA:246,1\nDA:247,1\nDA:248,1\nDA:249,1\nDA:250,1\nDA:251,2\nDA:252,1\nDA:256,0\nDA:258,0\nDA:259,0\nDA:260,0\nDA:262,0\nDA:282,1\nDA:295,1\nDA:296,1\nDA:297,1\nDA:299,1\nDA:300,1\nDA:301,1\nDA:302,1\nDA:303,1\nDA:304,1\nDA:305,1\nDA:306,1\nDA:310,0\nDA:311,0\nDA:313,0\nDA:314,0\nDA:316,0\nDA:318,0\nDA:319,0\nDA:321,0\nDA:336,1\nDA:338,1\nDA:339,1\nDA:340,1\nDA:342,1\nDA:343,1\nDA:344,1\nDA:348,0\nDA:349,0\nDA:351,0\nDA:352,0\nDA:354,0\nDA:355,0\nDA:365,0\nDA:373,0\nDA:375,0\nDA:377,0\nDA:378,0\nDA:379,0\nDA:381,0\nDA:382,0\nDA:383,0\nDA:385,0\nDA:386,0\nDA:387,0\nDA:389,0\nDA:390,0\nDA:391,0\nDA:393,0\nDA:394,0\nDA:397,0\nDA:399,0\nDA:400,0\nDA:401,0\nDA:402,0\nDA:403,0\nDA:404,0\nDA:405,0\nDA:407,0\nDA:408,0\nDA:409,0\nDA:411,0\nDA:412,0\nDA:414,0\nDA:415,0\nDA:417,0\nDA:418,0\nDA:420,0\nDA:421,0\nDA:427,0\nDA:428,0\nDA:429,0\nDA:430,0\nDA:431,0\nDA:434,0\nLF:165\nLH:86\nend_of_record\nSF:lib/generated/l10n/l10n_de.dart\nDA:9,0\nDA:11,0\nDA:15,0\nDA:18,0\nDA:22,0\nDA:25,0\nDA:28,0\nDA:31,0\nDA:35,0\nDA:39,0\nDA:42,0\nDA:45,0\nDA:48,0\nDA:52,0\nDA:56,0\nDA:59,0\nDA:63,0\nDA:66,0\nDA:69,0\nDA:72,0\nDA:75,0\nDA:78,0\nDA:81,0\nDA:84,0\nDA:88,0\nDA:91,0\nDA:94,0\nDA:98,0\nDA:101,0\nDA:104,0\nDA:108,0\nDA:111,0\nDA:114,0\nDA:118,0\nDA:121,0\nDA:124,0\nDA:128,0\nDA:131,0\nDA:134,0\nDA:137,0\nDA:140,0\nDA:144,0\nDA:147,0\nDA:150,0\nDA:153,0\nDA:156,0\nDA:159,0\nDA:162,0\nDA:164,0\nDA:167,0\nDA:171,0\nDA:173,0\nDA:176,0\nDA:179,0\nDA:184,0\nDA:187,0\nDA:189,0\nDA:192,0\nDA:195,0\nDA:199,0\nDA:202,0\nDA:205,0\nDA:209,0\nDA:212,0\nDA:215,0\nDA:218,0\nDA:221,0\nDA:224,0\nDA:227,0\nDA:231,0\nDA:234,0\nDA:238,0\nDA:240,0\nDA:243,0\nDA:246,0\nDA:249,0\nDA:252,0\nDA:255,0\nDA:258,0\nDA:261,0\nDA:264,0\nDA:268,0\nDA:271,0\nDA:274,0\nDA:277,0\nDA:280,0\nDA:283,0\nDA:286,0\nDA:288,0\nDA:291,0\nDA:294,0\nDA:297,0\nDA:300,0\nDA:303,0\nDA:306,0\nDA:309,0\nDA:313,0\nDA:316,0\nDA:319,0\nDA:322,0\nDA:325,0\nDA:329,0\nDA:332,0\nDA:335,0\nDA:339,0\nDA:342,0\nDA:345,0\nDA:348,0\nDA:350,0\nDA:353,0\nDA:356,0\nDA:359,0\nDA:362,0\nDA:366,0\nDA:369,0\nDA:372,0\nDA:375,0\nDA:379,0\nDA:382,0\nDA:386,0\nDA:389,0\nDA:392,0\nDA:395,0\nDA:398,0\nDA:401,0\nDA:404,0\nDA:408,0\nDA:411,0\nDA:414,0\nDA:417,0\nDA:420,0\nDA:423,0\nDA:427,0\nDA:430,0\nDA:434,0\nDA:438,0\nDA:440,0\nDA:443,0\nDA:446,0\nDA:449,0\nDA:452,0\nDA:456,0\nDA:459,0\nDA:462,0\nDA:465,0\nDA:468,0\nDA:471,0\nDA:474,0\nDA:477,0\nDA:480,0\nDA:483,0\nDA:487,0\nDA:491,0\nDA:495,0\nDA:498,0\nDA:501,0\nDA:504,0\nDA:507,0\nDA:510,0\nDA:513,0\nDA:516,0\nDA:519,0\nDA:522,0\nDA:525,0\nDA:528,0\nDA:531,0\nDA:535,0\nDA:538,0\nDA:541,0\nDA:544,0\nDA:547,0\nDA:550,0\nDA:553,0\nDA:556,0\nDA:559,0\nDA:562,0\nDA:565,0\nDA:569,0\nDA:573,0\nDA:576,0\nDA:579,0\nDA:582,0\nDA:585,0\nDA:588,0\nDA:591,0\nDA:594,0\nDA:597,0\nDA:601,0\nDA:604,0\nDA:606,0\nDA:609,0\nDA:613,0\nDA:615,0\nDA:618,0\nDA:622,0\nDA:625,0\nDA:628,0\nDA:631,0\nDA:634,0\nDA:637,0\nDA:640,0\nDA:644,0\nDA:647,0\nDA:651,0\nDA:653,0\nDA:656,0\nDA:659,0\nDA:663,0\nDA:666,0\nDA:669,0\nDA:672,0\nDA:676,0\nDA:679,0\nDA:682,0\nDA:685,0\nDA:689,0\nDA:692,0\nDA:695,0\nDA:698,0\nDA:701,0\nDA:704,0\nDA:707,0\nDA:710,0\nDA:713,0\nDA:716,0\nDA:719,0\nDA:723,0\nDA:727,0\nDA:730,0\nDA:733,0\nDA:736,0\nDA:739,0\nDA:743,0\nDA:746,0\nDA:749,0\nDA:752,0\nDA:755,0\nDA:758,0\nDA:762,0\nDA:765,0\nDA:768,0\nDA:772,0\nDA:775,0\nDA:778,0\nDA:781,0\nDA:784,0\nDA:788,0\nDA:791,0\nDA:795,0\nLF:249\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n_es.dart\nDA:9,0\nDA:11,0\nDA:14,0\nDA:17,0\nDA:21,0\nDA:24,0\nDA:27,0\nDA:30,0\nDA:34,0\nDA:38,0\nDA:41,0\nDA:44,0\nDA:48,0\nDA:52,0\nDA:56,0\nDA:59,0\nDA:63,0\nDA:66,0\nDA:69,0\nDA:72,0\nDA:75,0\nDA:78,0\nDA:81,0\nDA:84,0\nDA:88,0\nDA:91,0\nDA:94,0\nDA:98,0\nDA:101,0\nDA:104,0\nDA:108,0\nDA:111,0\nDA:114,0\nDA:118,0\nDA:121,0\nDA:124,0\nDA:128,0\nDA:131,0\nDA:134,0\nDA:137,0\nDA:140,0\nDA:144,0\nDA:147,0\nDA:150,0\nDA:153,0\nDA:156,0\nDA:159,0\nDA:162,0\nDA:164,0\nDA:167,0\nDA:171,0\nDA:173,0\nDA:176,0\nDA:179,0\nDA:184,0\nDA:187,0\nDA:189,0\nDA:192,0\nDA:195,0\nDA:199,0\nDA:202,0\nDA:205,0\nDA:209,0\nDA:212,0\nDA:215,0\nDA:218,0\nDA:221,0\nDA:224,0\nDA:227,0\nDA:231,0\nDA:234,0\nDA:238,0\nDA:240,0\nDA:243,0\nDA:246,0\nDA:249,0\nDA:252,0\nDA:255,0\nDA:258,0\nDA:261,0\nDA:264,0\nDA:268,0\nDA:271,0\nDA:274,0\nDA:277,0\nDA:280,0\nDA:283,0\nDA:286,0\nDA:288,0\nDA:291,0\nDA:294,0\nDA:297,0\nDA:300,0\nDA:303,0\nDA:306,0\nDA:309,0\nDA:313,0\nDA:316,0\nDA:319,0\nDA:322,0\nDA:325,0\nDA:329,0\nDA:332,0\nDA:335,0\nDA:339,0\nDA:342,0\nDA:345,0\nDA:348,0\nDA:350,0\nDA:353,0\nDA:356,0\nDA:359,0\nDA:363,0\nDA:366,0\nDA:369,0\nDA:372,0\nDA:375,0\nDA:379,0\nDA:382,0\nDA:386,0\nDA:389,0\nDA:392,0\nDA:395,0\nDA:398,0\nDA:401,0\nDA:404,0\nDA:408,0\nDA:411,0\nDA:414,0\nDA:417,0\nDA:420,0\nDA:423,0\nDA:427,0\nDA:430,0\nDA:434,0\nDA:438,0\nDA:440,0\nDA:443,0\nDA:446,0\nDA:449,0\nDA:453,0\nDA:457,0\nDA:460,0\nDA:463,0\nDA:466,0\nDA:470,0\nDA:473,0\nDA:476,0\nDA:479,0\nDA:482,0\nDA:485,0\nDA:489,0\nDA:493,0\nDA:497,0\nDA:500,0\nDA:503,0\nDA:506,0\nDA:510,0\nDA:513,0\nDA:516,0\nDA:519,0\nDA:522,0\nDA:525,0\nDA:528,0\nDA:531,0\nDA:534,0\nDA:537,0\nDA:540,0\nDA:543,0\nDA:546,0\nDA:549,0\nDA:552,0\nDA:555,0\nDA:559,0\nDA:562,0\nDA:565,0\nDA:568,0\nDA:572,0\nDA:576,0\nDA:579,0\nDA:582,0\nDA:585,0\nDA:588,0\nDA:591,0\nDA:594,0\nDA:597,0\nDA:600,0\nDA:604,0\nDA:607,0\nDA:609,0\nDA:612,0\nDA:616,0\nDA:618,0\nDA:621,0\nDA:625,0\nDA:628,0\nDA:631,0\nDA:634,0\nDA:637,0\nDA:640,0\nDA:643,0\nDA:646,0\nDA:649,0\nDA:653,0\nDA:655,0\nDA:658,0\nDA:661,0\nDA:665,0\nDA:668,0\nDA:671,0\nDA:674,0\nDA:678,0\nDA:681,0\nDA:684,0\nDA:687,0\nDA:691,0\nDA:694,0\nDA:697,0\nDA:700,0\nDA:703,0\nDA:706,0\nDA:709,0\nDA:712,0\nDA:715,0\nDA:718,0\nDA:721,0\nDA:725,0\nDA:729,0\nDA:732,0\nDA:735,0\nDA:738,0\nDA:741,0\nDA:745,0\nDA:748,0\nDA:751,0\nDA:754,0\nDA:757,0\nDA:760,0\nDA:764,0\nDA:767,0\nDA:770,0\nDA:774,0\nDA:777,0\nDA:780,0\nDA:783,0\nDA:786,0\nDA:790,0\nDA:793,0\nDA:797,0\nLF:249\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n_fr.dart\nDA:9,0\nDA:11,0\nDA:14,0\nDA:17,0\nDA:21,0\nDA:24,0\nDA:27,0\nDA:30,0\nDA:34,0\nDA:38,0\nDA:41,0\nDA:44,0\nDA:48,0\nDA:52,0\nDA:56,0\nDA:59,0\nDA:63,0\nDA:66,0\nDA:69,0\nDA:72,0\nDA:75,0\nDA:78,0\nDA:81,0\nDA:84,0\nDA:88,0\nDA:91,0\nDA:94,0\nDA:98,0\nDA:101,0\nDA:104,0\nDA:108,0\nDA:111,0\nDA:114,0\nDA:118,0\nDA:121,0\nDA:124,0\nDA:128,0\nDA:131,0\nDA:134,0\nDA:137,0\nDA:140,0\nDA:144,0\nDA:147,0\nDA:150,0\nDA:153,0\nDA:156,0\nDA:159,0\nDA:162,0\nDA:164,0\nDA:167,0\nDA:171,0\nDA:173,0\nDA:176,0\nDA:179,0\nDA:184,0\nDA:187,0\nDA:189,0\nDA:192,0\nDA:195,0\nDA:199,0\nDA:202,0\nDA:205,0\nDA:209,0\nDA:212,0\nDA:215,0\nDA:218,0\nDA:221,0\nDA:224,0\nDA:227,0\nDA:231,0\nDA:234,0\nDA:238,0\nDA:240,0\nDA:243,0\nDA:246,0\nDA:249,0\nDA:252,0\nDA:255,0\nDA:258,0\nDA:261,0\nDA:264,0\nDA:268,0\nDA:271,0\nDA:274,0\nDA:277,0\nDA:280,0\nDA:283,0\nDA:286,0\nDA:288,0\nDA:291,0\nDA:294,0\nDA:297,0\nDA:300,0\nDA:303,0\nDA:306,0\nDA:309,0\nDA:313,0\nDA:316,0\nDA:319,0\nDA:322,0\nDA:325,0\nDA:329,0\nDA:332,0\nDA:335,0\nDA:339,0\nDA:342,0\nDA:345,0\nDA:348,0\nDA:350,0\nDA:353,0\nDA:356,0\nDA:359,0\nDA:362,0\nDA:365,0\nDA:368,0\nDA:371,0\nDA:374,0\nDA:378,0\nDA:381,0\nDA:385,0\nDA:388,0\nDA:391,0\nDA:394,0\nDA:397,0\nDA:400,0\nDA:404,0\nDA:408,0\nDA:411,0\nDA:414,0\nDA:417,0\nDA:420,0\nDA:424,0\nDA:428,0\nDA:431,0\nDA:435,0\nDA:439,0\nDA:441,0\nDA:444,0\nDA:447,0\nDA:450,0\nDA:454,0\nDA:458,0\nDA:461,0\nDA:464,0\nDA:467,0\nDA:471,0\nDA:474,0\nDA:477,0\nDA:480,0\nDA:483,0\nDA:486,0\nDA:490,0\nDA:494,0\nDA:498,0\nDA:501,0\nDA:504,0\nDA:507,0\nDA:511,0\nDA:514,0\nDA:517,0\nDA:520,0\nDA:523,0\nDA:526,0\nDA:529,0\nDA:532,0\nDA:535,0\nDA:538,0\nDA:541,0\nDA:544,0\nDA:547,0\nDA:550,0\nDA:553,0\nDA:556,0\nDA:560,0\nDA:563,0\nDA:566,0\nDA:569,0\nDA:573,0\nDA:577,0\nDA:580,0\nDA:583,0\nDA:586,0\nDA:589,0\nDA:592,0\nDA:595,0\nDA:598,0\nDA:601,0\nDA:605,0\nDA:608,0\nDA:610,0\nDA:613,0\nDA:617,0\nDA:619,0\nDA:622,0\nDA:626,0\nDA:629,0\nDA:632,0\nDA:635,0\nDA:638,0\nDA:641,0\nDA:644,0\nDA:648,0\nDA:651,0\nDA:655,0\nDA:657,0\nDA:660,0\nDA:663,0\nDA:667,0\nDA:670,0\nDA:673,0\nDA:676,0\nDA:680,0\nDA:683,0\nDA:686,0\nDA:689,0\nDA:693,0\nDA:696,0\nDA:699,0\nDA:702,0\nDA:705,0\nDA:708,0\nDA:711,0\nDA:714,0\nDA:717,0\nDA:720,0\nDA:723,0\nDA:727,0\nDA:731,0\nDA:734,0\nDA:737,0\nDA:740,0\nDA:743,0\nDA:747,0\nDA:750,0\nDA:753,0\nDA:756,0\nDA:759,0\nDA:762,0\nDA:766,0\nDA:769,0\nDA:772,0\nDA:776,0\nDA:779,0\nDA:782,0\nDA:785,0\nDA:788,0\nDA:792,0\nDA:795,0\nDA:799,0\nLF:249\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n_id.dart\nDA:9,0\nDA:11,0\nDA:15,0\nDA:18,0\nDA:22,0\nDA:25,0\nDA:28,0\nDA:31,0\nDA:35,0\nDA:39,0\nDA:42,0\nDA:45,0\nDA:48,0\nDA:52,0\nDA:55,0\nDA:58,0\nDA:62,0\nDA:65,0\nDA:68,0\nDA:71,0\nDA:74,0\nDA:77,0\nDA:80,0\nDA:83,0\nDA:87,0\nDA:90,0\nDA:93,0\nDA:97,0\nDA:100,0\nDA:103,0\nDA:107,0\nDA:110,0\nDA:113,0\nDA:117,0\nDA:120,0\nDA:123,0\nDA:127,0\nDA:130,0\nDA:133,0\nDA:136,0\nDA:139,0\nDA:143,0\nDA:146,0\nDA:149,0\nDA:152,0\nDA:155,0\nDA:158,0\nDA:161,0\nDA:163,0\nDA:166,0\nDA:170,0\nDA:172,0\nDA:175,0\nDA:178,0\nDA:183,0\nDA:186,0\nDA:188,0\nDA:191,0\nDA:194,0\nDA:198,0\nDA:201,0\nDA:204,0\nDA:208,0\nDA:211,0\nDA:214,0\nDA:217,0\nDA:220,0\nDA:223,0\nDA:226,0\nDA:230,0\nDA:233,0\nDA:237,0\nDA:239,0\nDA:242,0\nDA:245,0\nDA:248,0\nDA:251,0\nDA:254,0\nDA:257,0\nDA:260,0\nDA:263,0\nDA:267,0\nDA:270,0\nDA:273,0\nDA:276,0\nDA:279,0\nDA:282,0\nDA:285,0\nDA:287,0\nDA:290,0\nDA:293,0\nDA:296,0\nDA:299,0\nDA:302,0\nDA:305,0\nDA:308,0\nDA:312,0\nDA:315,0\nDA:318,0\nDA:321,0\nDA:324,0\nDA:328,0\nDA:331,0\nDA:334,0\nDA:338,0\nDA:341,0\nDA:344,0\nDA:347,0\nDA:349,0\nDA:352,0\nDA:355,0\nDA:358,0\nDA:361,0\nDA:364,0\nDA:367,0\nDA:370,0\nDA:373,0\nDA:377,0\nDA:380,0\nDA:384,0\nDA:387,0\nDA:390,0\nDA:393,0\nDA:396,0\nDA:399,0\nDA:402,0\nDA:406,0\nDA:409,0\nDA:412,0\nDA:415,0\nDA:418,0\nDA:422,0\nDA:425,0\nDA:428,0\nDA:432,0\nDA:436,0\nDA:438,0\nDA:441,0\nDA:444,0\nDA:447,0\nDA:450,0\nDA:454,0\nDA:457,0\nDA:460,0\nDA:463,0\nDA:466,0\nDA:469,0\nDA:472,0\nDA:475,0\nDA:478,0\nDA:481,0\nDA:485,0\nDA:489,0\nDA:493,0\nDA:496,0\nDA:499,0\nDA:502,0\nDA:506,0\nDA:509,0\nDA:512,0\nDA:515,0\nDA:518,0\nDA:521,0\nDA:524,0\nDA:527,0\nDA:530,0\nDA:533,0\nDA:536,0\nDA:539,0\nDA:542,0\nDA:545,0\nDA:548,0\nDA:551,0\nDA:554,0\nDA:557,0\nDA:560,0\nDA:563,0\nDA:567,0\nDA:570,0\nDA:573,0\nDA:576,0\nDA:579,0\nDA:582,0\nDA:585,0\nDA:588,0\nDA:591,0\nDA:594,0\nDA:598,0\nDA:601,0\nDA:603,0\nDA:606,0\nDA:610,0\nDA:612,0\nDA:615,0\nDA:618,0\nDA:621,0\nDA:624,0\nDA:627,0\nDA:630,0\nDA:633,0\nDA:636,0\nDA:639,0\nDA:642,0\nDA:646,0\nDA:648,0\nDA:651,0\nDA:654,0\nDA:658,0\nDA:661,0\nDA:664,0\nDA:667,0\nDA:671,0\nDA:674,0\nDA:677,0\nDA:680,0\nDA:684,0\nDA:687,0\nDA:690,0\nDA:693,0\nDA:696,0\nDA:699,0\nDA:702,0\nDA:705,0\nDA:708,0\nDA:711,0\nDA:714,0\nDA:718,0\nDA:721,0\nDA:724,0\nDA:727,0\nDA:730,0\nDA:733,0\nDA:737,0\nDA:740,0\nDA:743,0\nDA:746,0\nDA:749,0\nDA:752,0\nDA:756,0\nDA:759,0\nDA:762,0\nDA:765,0\nDA:768,0\nDA:771,0\nDA:774,0\nDA:777,0\nDA:781,0\nDA:784,0\nDA:788,0\nLF:249\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n_ja.dart\nDA:9,0\nDA:11,0\nDA:14,0\nDA:17,0\nDA:21,0\nDA:24,0\nDA:27,0\nDA:30,0\nDA:33,0\nDA:36,0\nDA:39,0\nDA:42,0\nDA:45,0\nDA:48,0\nDA:51,0\nDA:54,0\nDA:58,0\nDA:61,0\nDA:64,0\nDA:67,0\nDA:70,0\nDA:73,0\nDA:76,0\nDA:79,0\nDA:83,0\nDA:86,0\nDA:89,0\nDA:92,0\nDA:95,0\nDA:98,0\nDA:102,0\nDA:105,0\nDA:108,0\nDA:111,0\nDA:114,0\nDA:117,0\nDA:121,0\nDA:124,0\nDA:127,0\nDA:130,0\nDA:133,0\nDA:136,0\nDA:139,0\nDA:142,0\nDA:145,0\nDA:148,0\nDA:151,0\nDA:154,0\nDA:156,0\nDA:159,0\nDA:163,0\nDA:165,0\nDA:168,0\nDA:171,0\nDA:176,0\nDA:179,0\nDA:181,0\nDA:184,0\nDA:187,0\nDA:191,0\nDA:194,0\nDA:197,0\nDA:201,0\nDA:204,0\nDA:207,0\nDA:210,0\nDA:213,0\nDA:216,0\nDA:219,0\nDA:223,0\nDA:226,0\nDA:230,0\nDA:232,0\nDA:235,0\nDA:238,0\nDA:241,0\nDA:244,0\nDA:247,0\nDA:250,0\nDA:253,0\nDA:256,0\nDA:260,0\nDA:263,0\nDA:266,0\nDA:269,0\nDA:272,0\nDA:275,0\nDA:278,0\nDA:280,0\nDA:283,0\nDA:286,0\nDA:289,0\nDA:292,0\nDA:295,0\nDA:298,0\nDA:301,0\nDA:305,0\nDA:308,0\nDA:311,0\nDA:314,0\nDA:317,0\nDA:320,0\nDA:323,0\nDA:326,0\nDA:329,0\nDA:332,0\nDA:335,0\nDA:338,0\nDA:340,0\nDA:343,0\nDA:346,0\nDA:349,0\nDA:352,0\nDA:355,0\nDA:358,0\nDA:361,0\nDA:364,0\nDA:368,0\nDA:371,0\nDA:375,0\nDA:378,0\nDA:381,0\nDA:384,0\nDA:387,0\nDA:390,0\nDA:393,0\nDA:396,0\nDA:399,0\nDA:402,0\nDA:405,0\nDA:408,0\nDA:411,0\nDA:414,0\nDA:417,0\nDA:420,0\nDA:423,0\nDA:425,0\nDA:428,0\nDA:431,0\nDA:434,0\nDA:437,0\nDA:440,0\nDA:443,0\nDA:446,0\nDA:449,0\nDA:452,0\nDA:455,0\nDA:458,0\nDA:461,0\nDA:464,0\nDA:467,0\nDA:471,0\nDA:475,0\nDA:478,0\nDA:481,0\nDA:484,0\nDA:487,0\nDA:490,0\nDA:493,0\nDA:496,0\nDA:499,0\nDA:502,0\nDA:505,0\nDA:508,0\nDA:511,0\nDA:514,0\nDA:517,0\nDA:520,0\nDA:523,0\nDA:526,0\nDA:529,0\nDA:532,0\nDA:535,0\nDA:538,0\nDA:541,0\nDA:544,0\nDA:547,0\nDA:551,0\nDA:554,0\nDA:557,0\nDA:560,0\nDA:563,0\nDA:566,0\nDA:569,0\nDA:572,0\nDA:575,0\nDA:578,0\nDA:582,0\nDA:585,0\nDA:587,0\nDA:590,0\nDA:594,0\nDA:596,0\nDA:599,0\nDA:602,0\nDA:605,0\nDA:608,0\nDA:611,0\nDA:614,0\nDA:617,0\nDA:620,0\nDA:623,0\nDA:626,0\nDA:629,0\nDA:631,0\nDA:634,0\nDA:637,0\nDA:640,0\nDA:643,0\nDA:646,0\nDA:649,0\nDA:653,0\nDA:656,0\nDA:659,0\nDA:662,0\nDA:666,0\nDA:669,0\nDA:672,0\nDA:675,0\nDA:678,0\nDA:681,0\nDA:684,0\nDA:687,0\nDA:690,0\nDA:693,0\nDA:696,0\nDA:700,0\nDA:703,0\nDA:706,0\nDA:709,0\nDA:712,0\nDA:715,0\nDA:718,0\nDA:721,0\nDA:724,0\nDA:727,0\nDA:730,0\nDA:733,0\nDA:737,0\nDA:740,0\nDA:743,0\nDA:746,0\nDA:749,0\nDA:752,0\nDA:755,0\nDA:758,0\nDA:761,0\nDA:764,0\nDA:768,0\nLF:249\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n_nl.dart\nDA:9,0\nDA:11,0\nDA:15,0\nDA:18,0\nDA:22,0\nDA:25,0\nDA:28,0\nDA:31,0\nDA:35,0\nDA:39,0\nDA:42,0\nDA:45,0\nDA:48,0\nDA:52,0\nDA:55,0\nDA:58,0\nDA:62,0\nDA:65,0\nDA:68,0\nDA:71,0\nDA:74,0\nDA:77,0\nDA:80,0\nDA:83,0\nDA:87,0\nDA:90,0\nDA:93,0\nDA:97,0\nDA:100,0\nDA:103,0\nDA:107,0\nDA:110,0\nDA:113,0\nDA:117,0\nDA:120,0\nDA:123,0\nDA:127,0\nDA:130,0\nDA:133,0\nDA:136,0\nDA:139,0\nDA:143,0\nDA:146,0\nDA:149,0\nDA:152,0\nDA:155,0\nDA:158,0\nDA:161,0\nDA:163,0\nDA:166,0\nDA:170,0\nDA:172,0\nDA:175,0\nDA:178,0\nDA:183,0\nDA:186,0\nDA:188,0\nDA:191,0\nDA:194,0\nDA:198,0\nDA:201,0\nDA:204,0\nDA:208,0\nDA:211,0\nDA:214,0\nDA:217,0\nDA:220,0\nDA:223,0\nDA:226,0\nDA:230,0\nDA:233,0\nDA:237,0\nDA:239,0\nDA:242,0\nDA:245,0\nDA:248,0\nDA:251,0\nDA:254,0\nDA:257,0\nDA:260,0\nDA:263,0\nDA:267,0\nDA:270,0\nDA:273,0\nDA:276,0\nDA:279,0\nDA:282,0\nDA:285,0\nDA:287,0\nDA:290,0\nDA:293,0\nDA:296,0\nDA:299,0\nDA:302,0\nDA:305,0\nDA:308,0\nDA:312,0\nDA:315,0\nDA:318,0\nDA:321,0\nDA:324,0\nDA:328,0\nDA:331,0\nDA:334,0\nDA:338,0\nDA:341,0\nDA:344,0\nDA:347,0\nDA:349,0\nDA:352,0\nDA:355,0\nDA:358,0\nDA:361,0\nDA:364,0\nDA:367,0\nDA:370,0\nDA:373,0\nDA:377,0\nDA:380,0\nDA:384,0\nDA:387,0\nDA:390,0\nDA:393,0\nDA:396,0\nDA:399,0\nDA:402,0\nDA:406,0\nDA:409,0\nDA:412,0\nDA:415,0\nDA:418,0\nDA:421,0\nDA:425,0\nDA:428,0\nDA:432,0\nDA:436,0\nDA:438,0\nDA:441,0\nDA:444,0\nDA:447,0\nDA:450,0\nDA:454,0\nDA:457,0\nDA:460,0\nDA:463,0\nDA:467,0\nDA:470,0\nDA:473,0\nDA:476,0\nDA:479,0\nDA:482,0\nDA:486,0\nDA:490,0\nDA:494,0\nDA:497,0\nDA:500,0\nDA:503,0\nDA:507,0\nDA:510,0\nDA:513,0\nDA:516,0\nDA:519,0\nDA:522,0\nDA:525,0\nDA:528,0\nDA:531,0\nDA:534,0\nDA:537,0\nDA:540,0\nDA:543,0\nDA:546,0\nDA:549,0\nDA:552,0\nDA:555,0\nDA:558,0\nDA:561,0\nDA:564,0\nDA:568,0\nDA:572,0\nDA:575,0\nDA:578,0\nDA:581,0\nDA:584,0\nDA:587,0\nDA:590,0\nDA:593,0\nDA:596,0\nDA:600,0\nDA:603,0\nDA:605,0\nDA:608,0\nDA:612,0\nDA:614,0\nDA:617,0\nDA:621,0\nDA:624,0\nDA:627,0\nDA:630,0\nDA:633,0\nDA:636,0\nDA:639,0\nDA:643,0\nDA:646,0\nDA:650,0\nDA:652,0\nDA:655,0\nDA:658,0\nDA:662,0\nDA:665,0\nDA:668,0\nDA:671,0\nDA:675,0\nDA:678,0\nDA:681,0\nDA:684,0\nDA:688,0\nDA:691,0\nDA:694,0\nDA:697,0\nDA:700,0\nDA:703,0\nDA:706,0\nDA:709,0\nDA:712,0\nDA:715,0\nDA:718,0\nDA:722,0\nDA:726,0\nDA:729,0\nDA:732,0\nDA:735,0\nDA:738,0\nDA:742,0\nDA:745,0\nDA:748,0\nDA:751,0\nDA:754,0\nDA:757,0\nDA:761,0\nDA:764,0\nDA:767,0\nDA:771,0\nDA:774,0\nDA:777,0\nDA:780,0\nDA:783,0\nDA:787,0\nDA:790,0\nDA:794,0\nLF:249\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n_pt.dart\nDA:9,0\nDA:11,0\nDA:14,0\nDA:17,0\nDA:21,0\nDA:24,0\nDA:27,0\nDA:30,0\nDA:34,0\nDA:38,0\nDA:41,0\nDA:44,0\nDA:48,0\nDA:52,0\nDA:56,0\nDA:59,0\nDA:63,0\nDA:66,0\nDA:69,0\nDA:72,0\nDA:75,0\nDA:78,0\nDA:81,0\nDA:84,0\nDA:88,0\nDA:91,0\nDA:94,0\nDA:97,0\nDA:100,0\nDA:103,0\nDA:107,0\nDA:110,0\nDA:113,0\nDA:117,0\nDA:120,0\nDA:123,0\nDA:127,0\nDA:130,0\nDA:133,0\nDA:136,0\nDA:139,0\nDA:143,0\nDA:146,0\nDA:149,0\nDA:152,0\nDA:155,0\nDA:158,0\nDA:161,0\nDA:163,0\nDA:166,0\nDA:170,0\nDA:172,0\nDA:175,0\nDA:178,0\nDA:183,0\nDA:186,0\nDA:188,0\nDA:191,0\nDA:194,0\nDA:198,0\nDA:201,0\nDA:204,0\nDA:208,0\nDA:211,0\nDA:214,0\nDA:217,0\nDA:220,0\nDA:223,0\nDA:226,0\nDA:230,0\nDA:233,0\nDA:237,0\nDA:239,0\nDA:242,0\nDA:245,0\nDA:248,0\nDA:251,0\nDA:254,0\nDA:257,0\nDA:260,0\nDA:263,0\nDA:267,0\nDA:270,0\nDA:273,0\nDA:276,0\nDA:279,0\nDA:283,0\nDA:286,0\nDA:288,0\nDA:291,0\nDA:294,0\nDA:297,0\nDA:300,0\nDA:303,0\nDA:306,0\nDA:309,0\nDA:313,0\nDA:316,0\nDA:319,0\nDA:322,0\nDA:325,0\nDA:328,0\nDA:331,0\nDA:334,0\nDA:338,0\nDA:341,0\nDA:344,0\nDA:347,0\nDA:349,0\nDA:352,0\nDA:355,0\nDA:358,0\nDA:362,0\nDA:365,0\nDA:368,0\nDA:371,0\nDA:374,0\nDA:378,0\nDA:381,0\nDA:385,0\nDA:388,0\nDA:391,0\nDA:394,0\nDA:397,0\nDA:400,0\nDA:403,0\nDA:407,0\nDA:410,0\nDA:413,0\nDA:416,0\nDA:419,0\nDA:422,0\nDA:426,0\nDA:429,0\nDA:433,0\nDA:437,0\nDA:439,0\nDA:442,0\nDA:445,0\nDA:448,0\nDA:451,0\nDA:455,0\nDA:458,0\nDA:461,0\nDA:464,0\nDA:467,0\nDA:470,0\nDA:473,0\nDA:476,0\nDA:479,0\nDA:482,0\nDA:486,0\nDA:490,0\nDA:494,0\nDA:497,0\nDA:500,0\nDA:503,0\nDA:506,0\nDA:509,0\nDA:512,0\nDA:515,0\nDA:518,0\nDA:521,0\nDA:524,0\nDA:527,0\nDA:530,0\nDA:533,0\nDA:536,0\nDA:539,0\nDA:542,0\nDA:545,0\nDA:548,0\nDA:551,0\nDA:555,0\nDA:558,0\nDA:561,0\nDA:564,0\nDA:568,0\nDA:571,0\nDA:574,0\nDA:577,0\nDA:580,0\nDA:583,0\nDA:586,0\nDA:589,0\nDA:592,0\nDA:595,0\nDA:599,0\nDA:602,0\nDA:604,0\nDA:607,0\nDA:611,0\nDA:613,0\nDA:616,0\nDA:620,0\nDA:623,0\nDA:626,0\nDA:629,0\nDA:632,0\nDA:635,0\nDA:638,0\nDA:641,0\nDA:644,0\nDA:648,0\nDA:650,0\nDA:653,0\nDA:656,0\nDA:660,0\nDA:663,0\nDA:666,0\nDA:669,0\nDA:673,0\nDA:676,0\nDA:679,0\nDA:682,0\nDA:686,0\nDA:689,0\nDA:692,0\nDA:695,0\nDA:698,0\nDA:701,0\nDA:704,0\nDA:707,0\nDA:710,0\nDA:713,0\nDA:716,0\nDA:720,0\nDA:724,0\nDA:727,0\nDA:730,0\nDA:733,0\nDA:736,0\nDA:740,0\nDA:743,0\nDA:746,0\nDA:749,0\nDA:752,0\nDA:755,0\nDA:759,0\nDA:762,0\nDA:765,0\nDA:768,0\nDA:771,0\nDA:774,0\nDA:777,0\nDA:780,0\nDA:784,0\nDA:787,0\nDA:791,0\nLF:249\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n_ru.dart\nDA:9,0\nDA:11,0\nDA:14,0\nDA:17,0\nDA:21,0\nDA:24,0\nDA:27,0\nDA:30,0\nDA:34,0\nDA:38,0\nDA:41,0\nDA:44,0\nDA:48,0\nDA:52,0\nDA:56,0\nDA:59,0\nDA:63,0\nDA:66,0\nDA:69,0\nDA:72,0\nDA:75,0\nDA:78,0\nDA:81,0\nDA:84,0\nDA:88,0\nDA:91,0\nDA:94,0\nDA:97,0\nDA:100,0\nDA:103,0\nDA:107,0\nDA:110,0\nDA:113,0\nDA:117,0\nDA:120,0\nDA:123,0\nDA:127,0\nDA:130,0\nDA:133,0\nDA:136,0\nDA:139,0\nDA:143,0\nDA:146,0\nDA:149,0\nDA:152,0\nDA:155,0\nDA:158,0\nDA:161,0\nDA:163,0\nDA:166,0\nDA:170,0\nDA:172,0\nDA:175,0\nDA:178,0\nDA:183,0\nDA:186,0\nDA:188,0\nDA:191,0\nDA:194,0\nDA:198,0\nDA:201,0\nDA:204,0\nDA:208,0\nDA:211,0\nDA:214,0\nDA:217,0\nDA:220,0\nDA:223,0\nDA:226,0\nDA:230,0\nDA:233,0\nDA:237,0\nDA:239,0\nDA:242,0\nDA:245,0\nDA:248,0\nDA:251,0\nDA:254,0\nDA:257,0\nDA:260,0\nDA:263,0\nDA:267,0\nDA:270,0\nDA:273,0\nDA:276,0\nDA:279,0\nDA:282,0\nDA:285,0\nDA:287,0\nDA:290,0\nDA:293,0\nDA:296,0\nDA:299,0\nDA:302,0\nDA:305,0\nDA:308,0\nDA:312,0\nDA:315,0\nDA:318,0\nDA:321,0\nDA:324,0\nDA:328,0\nDA:331,0\nDA:334,0\nDA:338,0\nDA:341,0\nDA:344,0\nDA:347,0\nDA:349,0\nDA:352,0\nDA:355,0\nDA:358,0\nDA:362,0\nDA:366,0\nDA:369,0\nDA:372,0\nDA:375,0\nDA:379,0\nDA:382,0\nDA:386,0\nDA:389,0\nDA:392,0\nDA:395,0\nDA:398,0\nDA:401,0\nDA:404,0\nDA:408,0\nDA:411,0\nDA:414,0\nDA:417,0\nDA:420,0\nDA:423,0\nDA:427,0\nDA:430,0\nDA:434,0\nDA:438,0\nDA:440,0\nDA:443,0\nDA:446,0\nDA:449,0\nDA:452,0\nDA:456,0\nDA:459,0\nDA:462,0\nDA:465,0\nDA:468,0\nDA:471,0\nDA:474,0\nDA:477,0\nDA:480,0\nDA:483,0\nDA:487,0\nDA:491,0\nDA:495,0\nDA:498,0\nDA:501,0\nDA:504,0\nDA:508,0\nDA:511,0\nDA:514,0\nDA:517,0\nDA:520,0\nDA:523,0\nDA:526,0\nDA:529,0\nDA:532,0\nDA:535,0\nDA:538,0\nDA:541,0\nDA:544,0\nDA:547,0\nDA:550,0\nDA:553,0\nDA:557,0\nDA:560,0\nDA:563,0\nDA:566,0\nDA:570,0\nDA:574,0\nDA:577,0\nDA:580,0\nDA:583,0\nDA:586,0\nDA:589,0\nDA:592,0\nDA:595,0\nDA:598,0\nDA:602,0\nDA:605,0\nDA:607,0\nDA:610,0\nDA:614,0\nDA:616,0\nDA:619,0\nDA:623,0\nDA:626,0\nDA:629,0\nDA:632,0\nDA:635,0\nDA:638,0\nDA:641,0\nDA:644,0\nDA:647,0\nDA:651,0\nDA:653,0\nDA:656,0\nDA:659,0\nDA:663,0\nDA:666,0\nDA:669,0\nDA:672,0\nDA:676,0\nDA:679,0\nDA:682,0\nDA:685,0\nDA:689,0\nDA:692,0\nDA:695,0\nDA:698,0\nDA:701,0\nDA:704,0\nDA:707,0\nDA:710,0\nDA:713,0\nDA:716,0\nDA:719,0\nDA:723,0\nDA:727,0\nDA:730,0\nDA:733,0\nDA:736,0\nDA:739,0\nDA:743,0\nDA:746,0\nDA:749,0\nDA:752,0\nDA:755,0\nDA:758,0\nDA:762,0\nDA:765,0\nDA:768,0\nDA:771,0\nDA:774,0\nDA:777,0\nDA:780,0\nDA:783,0\nDA:787,0\nDA:790,0\nDA:794,0\nLF:249\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n_tr.dart\nDA:9,0\nDA:11,0\nDA:14,0\nDA:17,0\nDA:21,0\nDA:24,0\nDA:27,0\nDA:30,0\nDA:34,0\nDA:38,0\nDA:41,0\nDA:44,0\nDA:47,0\nDA:51,0\nDA:54,0\nDA:57,0\nDA:61,0\nDA:64,0\nDA:67,0\nDA:70,0\nDA:73,0\nDA:76,0\nDA:79,0\nDA:82,0\nDA:86,0\nDA:89,0\nDA:92,0\nDA:96,0\nDA:99,0\nDA:102,0\nDA:106,0\nDA:109,0\nDA:112,0\nDA:116,0\nDA:119,0\nDA:122,0\nDA:126,0\nDA:129,0\nDA:132,0\nDA:135,0\nDA:138,0\nDA:142,0\nDA:145,0\nDA:148,0\nDA:151,0\nDA:154,0\nDA:157,0\nDA:160,0\nDA:162,0\nDA:165,0\nDA:169,0\nDA:171,0\nDA:174,0\nDA:177,0\nDA:182,0\nDA:185,0\nDA:187,0\nDA:190,0\nDA:193,0\nDA:197,0\nDA:200,0\nDA:203,0\nDA:207,0\nDA:210,0\nDA:213,0\nDA:216,0\nDA:219,0\nDA:222,0\nDA:225,0\nDA:229,0\nDA:232,0\nDA:236,0\nDA:238,0\nDA:241,0\nDA:244,0\nDA:247,0\nDA:250,0\nDA:253,0\nDA:256,0\nDA:259,0\nDA:262,0\nDA:266,0\nDA:269,0\nDA:272,0\nDA:275,0\nDA:278,0\nDA:281,0\nDA:284,0\nDA:286,0\nDA:289,0\nDA:292,0\nDA:295,0\nDA:298,0\nDA:301,0\nDA:304,0\nDA:307,0\nDA:311,0\nDA:314,0\nDA:317,0\nDA:320,0\nDA:323,0\nDA:327,0\nDA:330,0\nDA:333,0\nDA:337,0\nDA:340,0\nDA:343,0\nDA:346,0\nDA:348,0\nDA:351,0\nDA:354,0\nDA:357,0\nDA:360,0\nDA:363,0\nDA:366,0\nDA:369,0\nDA:372,0\nDA:376,0\nDA:379,0\nDA:383,0\nDA:386,0\nDA:389,0\nDA:392,0\nDA:395,0\nDA:398,0\nDA:401,0\nDA:405,0\nDA:408,0\nDA:411,0\nDA:414,0\nDA:417,0\nDA:420,0\nDA:424,0\nDA:427,0\nDA:431,0\nDA:435,0\nDA:437,0\nDA:440,0\nDA:443,0\nDA:446,0\nDA:449,0\nDA:453,0\nDA:456,0\nDA:459,0\nDA:462,0\nDA:465,0\nDA:468,0\nDA:471,0\nDA:474,0\nDA:477,0\nDA:480,0\nDA:484,0\nDA:488,0\nDA:492,0\nDA:495,0\nDA:498,0\nDA:501,0\nDA:505,0\nDA:508,0\nDA:511,0\nDA:514,0\nDA:517,0\nDA:520,0\nDA:523,0\nDA:526,0\nDA:529,0\nDA:532,0\nDA:535,0\nDA:538,0\nDA:541,0\nDA:544,0\nDA:547,0\nDA:550,0\nDA:553,0\nDA:556,0\nDA:559,0\nDA:562,0\nDA:566,0\nDA:570,0\nDA:573,0\nDA:576,0\nDA:579,0\nDA:582,0\nDA:585,0\nDA:588,0\nDA:591,0\nDA:594,0\nDA:598,0\nDA:601,0\nDA:603,0\nDA:606,0\nDA:610,0\nDA:612,0\nDA:615,0\nDA:618,0\nDA:621,0\nDA:624,0\nDA:627,0\nDA:630,0\nDA:633,0\nDA:636,0\nDA:640,0\nDA:643,0\nDA:647,0\nDA:649,0\nDA:652,0\nDA:655,0\nDA:659,0\nDA:662,0\nDA:665,0\nDA:668,0\nDA:672,0\nDA:675,0\nDA:678,0\nDA:681,0\nDA:685,0\nDA:688,0\nDA:691,0\nDA:694,0\nDA:697,0\nDA:700,0\nDA:703,0\nDA:706,0\nDA:709,0\nDA:712,0\nDA:715,0\nDA:719,0\nDA:722,0\nDA:725,0\nDA:728,0\nDA:731,0\nDA:734,0\nDA:738,0\nDA:741,0\nDA:744,0\nDA:747,0\nDA:750,0\nDA:753,0\nDA:757,0\nDA:760,0\nDA:763,0\nDA:766,0\nDA:769,0\nDA:772,0\nDA:775,0\nDA:778,0\nDA:782,0\nDA:785,0\nDA:789,0\nLF:249\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n_uk.dart\nDA:9,0\nDA:11,0\nDA:14,0\nDA:17,0\nDA:21,0\nDA:24,0\nDA:27,0\nDA:30,0\nDA:34,0\nDA:38,0\nDA:41,0\nDA:44,0\nDA:48,0\nDA:52,0\nDA:56,0\nDA:59,0\nDA:63,0\nDA:66,0\nDA:69,0\nDA:72,0\nDA:75,0\nDA:78,0\nDA:81,0\nDA:84,0\nDA:88,0\nDA:91,0\nDA:94,0\nDA:98,0\nDA:101,0\nDA:104,0\nDA:108,0\nDA:111,0\nDA:114,0\nDA:118,0\nDA:121,0\nDA:124,0\nDA:128,0\nDA:131,0\nDA:134,0\nDA:137,0\nDA:140,0\nDA:144,0\nDA:147,0\nDA:150,0\nDA:153,0\nDA:156,0\nDA:159,0\nDA:162,0\nDA:164,0\nDA:167,0\nDA:171,0\nDA:173,0\nDA:176,0\nDA:179,0\nDA:184,0\nDA:187,0\nDA:189,0\nDA:192,0\nDA:195,0\nDA:199,0\nDA:202,0\nDA:205,0\nDA:209,0\nDA:212,0\nDA:215,0\nDA:218,0\nDA:221,0\nDA:224,0\nDA:227,0\nDA:231,0\nDA:234,0\nDA:238,0\nDA:240,0\nDA:243,0\nDA:246,0\nDA:249,0\nDA:252,0\nDA:255,0\nDA:258,0\nDA:261,0\nDA:264,0\nDA:268,0\nDA:271,0\nDA:274,0\nDA:277,0\nDA:280,0\nDA:284,0\nDA:287,0\nDA:289,0\nDA:292,0\nDA:295,0\nDA:298,0\nDA:301,0\nDA:304,0\nDA:307,0\nDA:310,0\nDA:314,0\nDA:317,0\nDA:320,0\nDA:323,0\nDA:326,0\nDA:330,0\nDA:333,0\nDA:336,0\nDA:340,0\nDA:343,0\nDA:346,0\nDA:349,0\nDA:351,0\nDA:354,0\nDA:357,0\nDA:360,0\nDA:364,0\nDA:368,0\nDA:371,0\nDA:374,0\nDA:377,0\nDA:381,0\nDA:384,0\nDA:388,0\nDA:391,0\nDA:394,0\nDA:397,0\nDA:400,0\nDA:403,0\nDA:406,0\nDA:410,0\nDA:413,0\nDA:416,0\nDA:419,0\nDA:422,0\nDA:425,0\nDA:429,0\nDA:432,0\nDA:436,0\nDA:440,0\nDA:442,0\nDA:445,0\nDA:448,0\nDA:451,0\nDA:454,0\nDA:458,0\nDA:461,0\nDA:464,0\nDA:467,0\nDA:470,0\nDA:473,0\nDA:476,0\nDA:479,0\nDA:482,0\nDA:485,0\nDA:489,0\nDA:493,0\nDA:497,0\nDA:500,0\nDA:503,0\nDA:506,0\nDA:510,0\nDA:513,0\nDA:516,0\nDA:519,0\nDA:522,0\nDA:525,0\nDA:528,0\nDA:531,0\nDA:534,0\nDA:537,0\nDA:540,0\nDA:543,0\nDA:546,0\nDA:549,0\nDA:552,0\nDA:555,0\nDA:558,0\nDA:561,0\nDA:564,0\nDA:567,0\nDA:571,0\nDA:575,0\nDA:578,0\nDA:581,0\nDA:584,0\nDA:587,0\nDA:590,0\nDA:593,0\nDA:596,0\nDA:599,0\nDA:603,0\nDA:606,0\nDA:608,0\nDA:611,0\nDA:615,0\nDA:617,0\nDA:620,0\nDA:624,0\nDA:627,0\nDA:630,0\nDA:633,0\nDA:636,0\nDA:639,0\nDA:642,0\nDA:645,0\nDA:648,0\nDA:652,0\nDA:654,0\nDA:657,0\nDA:660,0\nDA:664,0\nDA:667,0\nDA:670,0\nDA:673,0\nDA:677,0\nDA:680,0\nDA:683,0\nDA:686,0\nDA:690,0\nDA:693,0\nDA:696,0\nDA:699,0\nDA:702,0\nDA:705,0\nDA:708,0\nDA:711,0\nDA:714,0\nDA:717,0\nDA:720,0\nDA:724,0\nDA:727,0\nDA:730,0\nDA:733,0\nDA:736,0\nDA:739,0\nDA:743,0\nDA:746,0\nDA:749,0\nDA:752,0\nDA:755,0\nDA:758,0\nDA:762,0\nDA:765,0\nDA:768,0\nDA:772,0\nDA:775,0\nDA:778,0\nDA:781,0\nDA:784,0\nDA:788,0\nDA:791,0\nDA:795,0\nLF:249\nLH:0\nend_of_record\nSF:lib/generated/l10n/l10n_zh.dart\nDA:9,0\nDA:11,0\nDA:14,0\nDA:17,0\nDA:20,0\nDA:23,0\nDA:26,0\nDA:29,0\nDA:32,0\nDA:35,0\nDA:38,0\nDA:41,0\nDA:44,0\nDA:47,0\nDA:50,0\nDA:53,0\nDA:56,0\nDA:59,0\nDA:62,0\nDA:65,0\nDA:68,0\nDA:71,0\nDA:74,0\nDA:77,0\nDA:81,0\nDA:84,0\nDA:87,0\nDA:90,0\nDA:93,0\nDA:96,0\nDA:100,0\nDA:103,0\nDA:106,0\nDA:109,0\nDA:112,0\nDA:115,0\nDA:119,0\nDA:122,0\nDA:125,0\nDA:128,0\nDA:131,0\nDA:134,0\nDA:137,0\nDA:140,0\nDA:143,0\nDA:146,0\nDA:149,0\nDA:152,0\nDA:154,0\nDA:157,0\nDA:161,0\nDA:163,0\nDA:166,0\nDA:169,0\nDA:174,0\nDA:177,0\nDA:179,0\nDA:182,0\nDA:185,0\nDA:188,0\nDA:191,0\nDA:194,0\nDA:197,0\nDA:200,0\nDA:203,0\nDA:206,0\nDA:209,0\nDA:212,0\nDA:215,0\nDA:218,0\nDA:221,0\nDA:225,0\nDA:227,0\nDA:230,0\nDA:233,0\nDA:236,0\nDA:239,0\nDA:242,0\nDA:245,0\nDA:248,0\nDA:251,0\nDA:254,0\nDA:257,0\nDA:260,0\nDA:263,0\nDA:266,0\nDA:269,0\nDA:272,0\nDA:274,0\nDA:277,0\nDA:280,0\nDA:283,0\nDA:286,0\nDA:289,0\nDA:292,0\nDA:295,0\nDA:299,0\nDA:302,0\nDA:305,0\nDA:308,0\nDA:311,0\nDA:314,0\nDA:317,0\nDA:320,0\nDA:323,0\nDA:326,0\nDA:329,0\nDA:332,0\nDA:334,0\nDA:337,0\nDA:340,0\nDA:343,0\nDA:346,0\nDA:349,0\nDA:352,0\nDA:355,0\nDA:358,0\nDA:362,0\nDA:365,0\nDA:369,0\nDA:372,0\nDA:375,0\nDA:378,0\nDA:381,0\nDA:384,0\nDA:387,0\nDA:390,0\nDA:393,0\nDA:396,0\nDA:399,0\nDA:402,0\nDA:405,0\nDA:408,0\nDA:411,0\nDA:414,0\nDA:417,0\nDA:419,0\nDA:422,0\nDA:425,0\nDA:428,0\nDA:431,0\nDA:434,0\nDA:437,0\nDA:440,0\nDA:443,0\nDA:446,0\nDA:449,0\nDA:452,0\nDA:455,0\nDA:458,0\nDA:461,0\nDA:464,0\nDA:467,0\nDA:470,0\nDA:473,0\nDA:476,0\nDA:479,0\nDA:482,0\nDA:485,0\nDA:488,0\nDA:491,0\nDA:494,0\nDA:497,0\nDA:500,0\nDA:503,0\nDA:506,0\nDA:509,0\nDA:512,0\nDA:515,0\nDA:518,0\nDA:521,0\nDA:524,0\nDA:527,0\nDA:530,0\nDA:533,0\nDA:536,0\nDA:539,0\nDA:543,0\nDA:546,0\nDA:549,0\nDA:552,0\nDA:555,0\nDA:558,0\nDA:561,0\nDA:564,0\nDA:567,0\nDA:570,0\nDA:573,0\nDA:576,0\nDA:578,0\nDA:581,0\nDA:585,0\nDA:587,0\nDA:590,0\nDA:593,0\nDA:596,0\nDA:599,0\nDA:602,0\nDA:605,0\nDA:608,0\nDA:611,0\nDA:614,0\nDA:617,0\nDA:620,0\nDA:622,0\nDA:625,0\nDA:628,0\nDA:631,0\nDA:634,0\nDA:637,0\nDA:640,0\nDA:643,0\nDA:646,0\nDA:649,0\nDA:652,0\nDA:655,0\nDA:658,0\nDA:661,0\nDA:664,0\nDA:667,0\nDA:670,0\nDA:673,0\nDA:676,0\nDA:679,0\nDA:682,0\nDA:685,0\nDA:688,0\nDA:691,0\nDA:694,0\nDA:697,0\nDA:700,0\nDA:703,0\nDA:706,0\nDA:709,0\nDA:712,0\nDA:715,0\nDA:718,0\nDA:721,0\nDA:724,0\nDA:727,0\nDA:730,0\nDA:733,0\nDA:736,0\nDA:739,0\nDA:742,0\nDA:745,0\nDA:748,0\nDA:751,0\nDA:754,0\nDA:761,0\nDA:763,0\nDA:766,0\nDA:769,0\nDA:772,0\nDA:775,0\nDA:778,0\nDA:781,0\nDA:784,0\nDA:787,0\nDA:790,0\nDA:793,0\nDA:796,0\nDA:799,0\nDA:802,0\nDA:805,0\nDA:808,0\nDA:811,0\nDA:814,0\nDA:817,0\nDA:820,0\nDA:823,0\nDA:826,0\nDA:829,0\nDA:833,0\nDA:836,0\nDA:839,0\nDA:842,0\nDA:845,0\nDA:848,0\nDA:852,0\nDA:855,0\nDA:858,0\nDA:861,0\nDA:864,0\nDA:867,0\nDA:871,0\nDA:874,0\nDA:877,0\nDA:880,0\nDA:883,0\nDA:886,0\nDA:889,0\nDA:892,0\nDA:895,0\nDA:898,0\nDA:901,0\nDA:904,0\nDA:906,0\nDA:909,0\nDA:913,0\nDA:915,0\nDA:918,0\nDA:921,0\nDA:926,0\nDA:929,0\nDA:931,0\nDA:934,0\nDA:937,0\nDA:940,0\nDA:943,0\nDA:946,0\nDA:949,0\nDA:952,0\nDA:955,0\nDA:958,0\nDA:961,0\nDA:964,0\nDA:967,0\nDA:970,0\nDA:973,0\nDA:977,0\nDA:979,0\nDA:982,0\nDA:985,0\nDA:988,0\nDA:991,0\nDA:994,0\nDA:997,0\nDA:1000,0\nDA:1003,0\nDA:1006,0\nDA:1009,0\nDA:1012,0\nDA:1015,0\nDA:1018,0\nDA:1021,0\nDA:1024,0\nDA:1026,0\nDA:1029,0\nDA:1032,0\nDA:1035,0\nDA:1038,0\nDA:1041,0\nDA:1044,0\nDA:1047,0\nDA:1051,0\nDA:1054,0\nDA:1057,0\nDA:1060,0\nDA:1063,0\nDA:1066,0\nDA:1069,0\nDA:1072,0\nDA:1075,0\nDA:1078,0\nDA:1081,0\nDA:1084,0\nDA:1086,0\nDA:1089,0\nDA:1092,0\nDA:1095,0\nDA:1098,0\nDA:1101,0\nDA:1104,0\nDA:1107,0\nDA:1110,0\nDA:1114,0\nDA:1117,0\nDA:1121,0\nDA:1124,0\nDA:1127,0\nDA:1130,0\nDA:1133,0\nDA:1136,0\nDA:1139,0\nDA:1142,0\nDA:1145,0\nDA:1148,0\nDA:1151,0\nDA:1154,0\nDA:1157,0\nDA:1160,0\nDA:1163,0\nDA:1166,0\nDA:1169,0\nDA:1171,0\nDA:1174,0\nDA:1177,0\nDA:1180,0\nDA:1183,0\nDA:1186,0\nDA:1189,0\nDA:1192,0\nDA:1195,0\nDA:1198,0\nDA:1201,0\nDA:1204,0\nDA:1207,0\nDA:1210,0\nDA:1213,0\nDA:1216,0\nDA:1219,0\nDA:1222,0\nDA:1225,0\nDA:1228,0\nDA:1231,0\nDA:1234,0\nDA:1237,0\nDA:1240,0\nDA:1243,0\nDA:1246,0\nDA:1249,0\nDA:1252,0\nDA:1255,0\nDA:1258,0\nDA:1261,0\nDA:1264,0\nDA:1267,0\nDA:1270,0\nDA:1273,0\nDA:1276,0\nDA:1279,0\nDA:1282,0\nDA:1285,0\nDA:1288,0\nDA:1291,0\nDA:1295,0\nDA:1298,0\nDA:1301,0\nDA:1304,0\nDA:1307,0\nDA:1310,0\nDA:1313,0\nDA:1316,0\nDA:1319,0\nDA:1322,0\nDA:1325,0\nDA:1328,0\nDA:1330,0\nDA:1333,0\nDA:1337,0\nDA:1339,0\nDA:1342,0\nDA:1345,0\nDA:1348,0\nDA:1351,0\nDA:1354,0\nDA:1357,0\nDA:1360,0\nDA:1363,0\nDA:1366,0\nDA:1369,0\nDA:1372,0\nDA:1374,0\nDA:1377,0\nDA:1380,0\nDA:1383,0\nDA:1386,0\nDA:1389,0\nDA:1392,0\nDA:1395,0\nDA:1398,0\nDA:1401,0\nDA:1404,0\nDA:1407,0\nDA:1410,0\nDA:1413,0\nDA:1416,0\nDA:1419,0\nDA:1422,0\nDA:1425,0\nDA:1428,0\nDA:1431,0\nDA:1434,0\nDA:1437,0\nDA:1440,0\nDA:1443,0\nDA:1446,0\nDA:1449,0\nDA:1452,0\nDA:1455,0\nDA:1458,0\nDA:1461,0\nDA:1464,0\nDA:1467,0\nDA:1470,0\nDA:1473,0\nDA:1476,0\nDA:1479,0\nDA:1482,0\nDA:1485,0\nDA:1488,0\nDA:1491,0\nDA:1494,0\nDA:1497,0\nDA:1500,0\nDA:1503,0\nDA:1506,0\nLF:498\nLH:0\nend_of_record\nSF:lib/data/model/server/proc.dart\nDA:18,1\nDA:47,1\nDA:61,1\nDA:62,2\nDA:63,1\nDA:64,3\nDA:65,3\nDA:66,1\nDA:67,1\nDA:68,3\nDA:69,1\nDA:70,1\nDA:71,3\nDA:72,1\nDA:73,1\nDA:74,3\nDA:78,0\nDA:79,0\nDA:80,0\nDA:81,0\nDA:82,0\nDA:83,0\nDA:84,0\nDA:85,0\nDA:86,0\nDA:87,0\nDA:88,0\nDA:89,0\nDA:90,0\nDA:94,0\nDA:96,0\nDA:99,0\nDA:100,0\nDA:101,0\nDA:110,2\nDA:112,1\nDA:113,5\nDA:114,1\nDA:116,1\nDA:117,2\nDA:118,3\nDA:119,1\nDA:120,1\nDA:121,1\nDA:122,1\nDA:123,1\nDA:124,1\nDA:125,1\nDA:126,1\nDA:127,1\nDA:128,1\nDA:129,1\nDA:130,1\nDA:133,1\nDA:134,1\nDA:135,3\nDA:136,1\nDA:137,1\nDA:139,2\nDA:141,0\nDA:142,0\nDA:147,1\nDA:148,1\nDA:150,0\nDA:151,0\nDA:153,0\nDA:154,0\nDA:156,0\nDA:157,0\nDA:159,0\nDA:160,0\nDA:163,2\nDA:170,1\nDA:171,1\nDA:172,2\nLF:75\nLH:47\nend_of_record\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": "docs/.gitignore",
    "content": "# build output\ndist/\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n\n# environment variables\n.env\n.env.production\n\n# macOS-specific files\n.DS_Store\n"
  },
  {
    "path": "docs/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"astro-build.astro-vscode\"],\n  \"unwantedRecommendations\": []\n}\n"
  },
  {
    "path": "docs/.vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"command\": \"./node_modules/.bin/astro dev\",\n      \"name\": \"Development server\",\n      \"request\": \"launch\",\n      \"type\": \"node-terminal\"\n    }\n  ]\n}\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Starlight Starter Kit: Basics\n\n[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)\n\n```\nnpm create astro@latest -- --template starlight\n```\n\n> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!\n\n## 🚀 Project Structure\n\nInside of your Astro + Starlight project, you'll see the following folders and files:\n\n```\n.\n├── public/\n├── src/\n│   ├── assets/\n│   ├── content/\n│   │   └── docs/\n│   └── content.config.ts\n├── astro.config.mjs\n├── package.json\n└── tsconfig.json\n```\n\nStarlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.\n\nImages can be added to `src/assets/` and embedded in Markdown with a relative link.\n\nStatic assets, like favicons, can be placed in the `public/` directory.\n\n## 🧞 Commands\n\nAll commands are run from the root of the project, from a terminal:\n\n| Command                   | Action                                           |\n| :------------------------ | :----------------------------------------------- |\n| `npm install`             | Installs dependencies                            |\n| `npm run dev`             | Starts local dev server at `localhost:4321`      |\n| `npm run build`           | Build your production site to `./dist/`          |\n| `npm run preview`         | Preview your build locally, before deploying     |\n| `npm run astro ...`       | Run CLI commands like `astro add`, `astro check` |\n| `npm run astro -- --help` | Get help using the Astro CLI                     |\n\n## 👀 Want to learn more?\n\nCheck out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).\n"
  },
  {
    "path": "docs/astro.config.mjs",
    "content": "// @ts-check\nimport { defineConfig } from 'astro/config';\nimport starlight from '@astrojs/starlight';\n\n// https://astro.build/config\nexport default defineConfig({\n\tintegrations: [\n\t\tstarlight({\n\t\t\ttitle: 'Server Box',\n\t\t\tdescription: 'A comprehensive cross-platform server management application built with Flutter',\n\t\t\tdefaultLocale: 'root',\n\t\t\tlocales: {\n\t\t\t\troot: {\n\t\t\t\t\tlabel: 'English',\n\t\t\t\t\tlang: 'en',\n\t\t\t\t},\n\t\t\t\tzh: {\n\t\t\t\t\tlabel: '简体中文',\n\t\t\t\t\tlang: 'zh',\n\t\t\t\t},\n\t\t\t\tde: {\n\t\t\t\t\tlabel: 'Deutsch',\n\t\t\t\t\tlang: 'de',\n\t\t\t\t},\n\t\t\t\tfr: {\n\t\t\t\t\tlabel: 'Français',\n\t\t\t\t\tlang: 'fr',\n\t\t\t\t},\n\t\t\t\tes: {\n\t\t\t\t\tlabel: 'Español',\n\t\t\t\t\tlang: 'es',\n\t\t\t\t},\n\t\t\t\tja: {\n\t\t\t\t\tlabel: '日本語',\n\t\t\t\t\tlang: 'ja',\n\t\t\t\t},\n\t\t\t},\n\t\t\tlogo: {\n\t\t\t\tsrc: './src/assets/logo.svg',\n\t\t\t},\n\t\t\tsocial: [\n\t\t\t\t{ icon: 'github', label: 'GitHub', href: 'https://github.com/lollipopkit/flutter_server_box' },\n\t\t\t],\n\t\t\tsidebar: [\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Getting Started',\n\t\t\t\t\ttranslations: {\n\t\t\t\t\t\tzh: '开始使用',\n\t\t\t\t\t\tde: 'Erste Schritte',\n\t\t\t\t\t\tfr: 'Mise en route',\n\t\t\t\t\t\tes: 'Primeros pasos',\n\t\t\t\t\t\tja: 'はじめに',\n\t\t\t\t\t},\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{ label: 'Introduction', translations: { zh: '介绍', de: 'Einführung', fr: 'Introduction', es: 'Introducción', ja: 'はじめに' }, slug: 'introduction' },\n\t\t\t\t\t\t{ label: 'Installation', translations: { zh: '安装', de: 'Installation', fr: 'Installation', es: 'Instalación', ja: 'インストール' }, slug: 'installation' },\n\t\t\t\t\t\t{ label: 'Quick Start', translations: { zh: '快速开始', de: 'Schnellstart', fr: 'Démarrage rapide', es: 'Inicio rápido', ja: 'クイックスタート' }, slug: 'quick-start' },\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Platform Features',\n\t\t\t\t\ttranslations: {\n\t\t\t\t\t\tzh: '平台特性',\n\t\t\t\t\t\tde: 'Plattformfunktionen',\n\t\t\t\t\t\tfr: 'Fonctionnalités de la plateforme',\n\t\t\t\t\t\tes: 'Características de la plataforma',\n\t\t\t\t\t\tja: 'プラットフォーム機能',\n\t\t\t\t\t},\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{ label: 'Mobile', translations: { zh: '移动端', de: 'Mobil', fr: 'Mobile', es: 'Móvil', ja: 'モバイル' }, slug: 'platforms/mobile' },\n\t\t\t\t\t\t{ label: 'Desktop', translations: { zh: '桌面端', de: 'Desktop', fr: 'Bureau', es: 'Escritorio', ja: 'デスクトップ' }, slug: 'platforms/desktop' },\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Advanced',\n\t\t\t\t\ttranslations: {\n\t\t\t\t\t\tzh: '进阶',\n\t\t\t\t\t\tde: 'Fortgeschritten',\n\t\t\t\t\t\tfr: 'Avancé',\n\t\t\t\t\t\tes: 'Avanzado',\n\t\t\t\t\t\tja: '高度な設定',\n\t\t\t\t\t},\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{ label: 'Bulk Import Servers', translations: { zh: '批量导入服务器', de: 'Server-Massenimport', fr: 'Importation massive de serveurs', es: 'Importación masiva de servidores', ja: 'サーバーの一括インポート' }, slug: 'advanced/bulk-import' },\n\t\t\t\t\t\t{ label: 'Widget Setup', translations: { zh: '小组件设置', de: 'Widget-Einrichtung', fr: 'Configuration du widget', es: 'Configuración de widgets', ja: 'ウィジェット設定' }, slug: 'advanced/widgets' },\n\t\t\t\t\t\t{ label: 'Custom Commands', translations: { zh: '自定义命令', de: 'Benutzerdefinierte Befehle', fr: 'Commandes personnalisées', es: 'Comandos personalizados', ja: 'カスタムコマンド' }, slug: 'advanced/custom-commands' },\n\t\t\t\t\t\t{ label: 'Custom Logo', translations: { zh: '自定义 Logo', de: 'Benutzerdefiniertes Logo', fr: 'Logo personnalisé', es: 'Logo personalizado', ja: 'カスタムロゴ' }, slug: 'advanced/custom-logo' },\n\t\t\t\t\t\t{ label: 'JSON Settings', translations: { zh: 'JSON 设置', de: 'JSON-Einstellungen', fr: 'Paramètres JSON', es: 'Ajustes JSON', ja: 'JSON 設定' }, slug: 'advanced/json-settings' },\n\t\t\t\t\t\t{ label: 'Common Issues', translations: { zh: '常见问题', de: 'Häufige Probleme', fr: 'Problèmes courants', es: 'Problemas comunes', ja: 'よくある質問' }, slug: 'advanced/troubleshooting' },\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: 'How It Works',\n\t\t\t\t\ttranslations: {\n\t\t\t\t\t\tzh: '工作原理',\n\t\t\t\t\t\tde: 'Wie es funktioniert',\n\t\t\t\t\t\tfr: 'Comment ça marche',\n\t\t\t\t\t\tes: 'Cómo funciona',\n\t\t\t\t\t\tja: '仕組み',\n\t\t\t\t\t},\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{ label: 'Architecture', translations: { zh: '架构', de: 'Architektur', fr: 'Architecture', es: 'Arquitectura', ja: 'アーキテクチャ' }, slug: 'principles/architecture' },\n\t\t\t\t\t\t{ label: 'SSH Connection', translations: { zh: 'SSH 连接', de: 'SSH-Verbindung', fr: 'Connexion SSH', es: 'Conexión SSH', ja: 'SSH 接続' }, slug: 'principles/ssh' },\n\t\t\t\t\t\t{ label: 'Terminal', translations: { zh: '终端', de: 'Terminal', fr: 'Terminal', es: 'Terminal', ja: 'ターミナル' }, slug: 'principles/terminal' },\n\t\t\t\t\t\t{ label: 'SFTP', translations: { zh: 'SFTP', de: 'SFTP', fr: 'SFTP', es: 'SFTP', ja: 'SFTP' }, slug: 'principles/sftp' },\n\t\t\t\t\t\t{ label: 'State Management', translations: { zh: '状态管理', de: 'Zustandsverwaltung', fr: 'Gestion d\\'état', es: 'Gestión de estado', ja: '状態管理' }, slug: 'principles/state' },\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Development',\n\t\t\t\t\ttranslations: {\n\t\t\t\t\t\tzh: '开发',\n\t\t\t\t\t\tde: 'Entwicklung',\n\t\t\t\t\t\tfr: 'Développement',\n\t\t\t\t\t\tes: 'Desarrollo',\n\t\t\t\t\t\tja: '開発',\n\t\t\t\t\t},\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{ label: 'Project Structure', translations: { zh: '项目结构', de: 'Projektstruktur', fr: 'Structure du projet', es: 'Estructura del proyecto', ja: 'プロジェクト構造' }, slug: 'development/structure' },\n\t\t\t\t\t\t{ label: 'Architecture', translations: { zh: '架构', de: 'Architektur', fr: 'Architecture', es: 'Arquitectura', ja: 'アーキテクチャ' }, slug: 'development/architecture' },\n\t\t\t\t\t\t{ label: 'State Management', translations: { zh: '状态管理', de: 'Zustandsverwaltung', fr: 'Gestion d\\'état', es: 'Gestión de estado', ja: '状態管理' }, slug: 'development/state' },\n\t\t\t\t\t\t{ label: 'Code Generation', translations: { zh: '代码生成', de: 'Code-Generierung', fr: 'Génération de code', es: 'Generación de código', ja: 'コード生成' }, slug: 'development/codegen' },\n\t\t\t\t\t\t{ label: 'Building', translations: { zh: '构建', de: 'Bauen', fr: 'Construction', es: 'Construcción', ja: 'ビルド' }, slug: 'development/building' },\n\t\t\t\t\t\t{ label: 'Testing', translations: { zh: '测试', de: 'Testen', fr: 'Tests', es: 'Pruebas', ja: 'テスト' }, slug: 'development/testing' },\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t],\n\t\t\tcustomCss: ['./src/styles/custom.css'],\n\t\t}),\n\t],\n});\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"docs\",\n  \"type\": \"module\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"start\": \"astro dev\",\n    \"build\": \"astro build\",\n    \"preview\": \"astro preview\",\n    \"astro\": \"astro\"\n  },\n  \"dependencies\": {\n    \"@astrojs/starlight\": \"^0.37.4\",\n    \"astro\": \"^5.6.1\",\n    \"sharp\": \"^0.34.2\"\n  }\n}"
  },
  {
    "path": "docs/src/content/docs/advanced/bulk-import.md",
    "content": "---\ntitle: Bulk Import Servers\ndescription: Import multiple servers from JSON file\n---\n\nImport multiple server configurations at once using a JSON file.\n\n## JSON Format\n\n:::danger[Security Warning]\n**Never store plaintext passwords in files!** This JSON example shows a password field for demonstration only, but you should:\n\n- **Prefer SSH keys** (`keyId`) instead of `pwd` - they're more secure\n- **Use secret managers** or environment variables if you must use passwords\n- **Delete the file immediately** after import - don't leave credentials lying around\n- **Add to .gitignore** - never commit credential files to version control\n:::\n\n```json\n[\n  {\n    \"name\": \"My Server\",\n    \"ip\": \"example.com\",\n    \"port\": 22,\n    \"user\": \"root\",\n    \"pwd\": \"password\",\n    \"keyId\": \"\",\n    \"tags\": [\"production\"],\n    \"autoConnect\": false\n  }\n]\n```\n\n## Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Display name |\n| `ip` | Yes | Domain or IP address |\n| `port` | Yes | SSH port (usually 22) |\n| `user` | Yes | SSH username |\n| `pwd` | No | Password (avoid - use SSH keys instead) |\n| `keyId` | No | SSH key name (from Private Keys - recommended) |\n| `tags` | No | Organization tags |\n| `autoConnect` | No | Auto-connect on startup |\n\n## Import Steps\n\n1. Create JSON file with server configurations\n2. Settings → Backup → Bulk Import Servers\n3. Select your JSON file\n4. Confirm import\n\n## Example\n\n```json\n[\n  {\n    \"name\": \"Production\",\n    \"ip\": \"prod.example.com\",\n    \"port\": 22,\n    \"user\": \"admin\",\n    \"keyId\": \"my-key\",\n    \"tags\": [\"production\", \"web\"]\n  },\n  {\n    \"name\": \"Development\",\n    \"ip\": \"dev.example.com\",\n    \"port\": 2222,\n    \"user\": \"dev\",\n    \"keyId\": \"dev-key\",\n    \"tags\": [\"development\"]\n  }\n]\n```\n\n## Tips\n\n- **Use SSH keys** instead of passwords when possible\n- **Test connection** after import\n- **Organize with tags** for easier management\n- **Delete JSON file** after import\n- **Never commit** JSON files with credentials to version control\n"
  },
  {
    "path": "docs/src/content/docs/advanced/custom-commands.md",
    "content": "---\ntitle: Custom Commands\ndescription: Display custom command output on server page\n---\n\nAdd custom shell commands to show their output on the server detail page.\n\n## Setup\n\n1. Server settings → Custom Commands\n2. Enter commands in JSON format\n\n## Basic Format\n\n```json\n{\n  \"Display Name\": \"shell command\"\n}\n```\n\n**Example:**\n```json\n{\n  \"Memory\": \"free -h\",\n  \"Disk\": \"df -h\",\n  \"Uptime\": \"uptime\"\n}\n```\n\n## Viewing Results\n\nAfter setup, custom commands appear on server detail page and refresh automatically.\n\n## Special Command Names\n\n### server_card_top_right\n\nDisplay on home page server card (top-right corner):\n\n```json\n{\n  \"server_card_top_right\": \"your-command-here\"\n}\n```\n\n## Tips\n\n**Use absolute paths:**\n```json\n{\"My Script\": \"/usr/local/bin/my-script.sh\"}\n```\n\n**Pipe commands:**\n```json\n{\"Top Process\": \"ps aux | sort -rk 3 | head -5\"}\n```\n\n**Format output:**\n```json\n{\"CPU Load\": \"uptime | awk -F'load average:' '{print $2}'\"}\n```\n\n**Keep commands fast:** Under 5 seconds for best experience\n\n**Limit output:**\n```json\n{\"Logs\": \"tail -20 /var/log/syslog\"}\n```\n\n## Security\n\nCommands run with SSH user permissions. Avoid commands that modify system state.\n"
  },
  {
    "path": "docs/src/content/docs/advanced/custom-logo.md",
    "content": "---\ntitle: Custom Server Logo\ndescription: Use custom images for server cards\n---\n\nDisplay custom logos on server cards using image URLs.\n\n## Setup\n\n1. Server settings → Custom Logo\n2. Enter image URL\n\n## URL Placeholders\n\n### {DIST} - Linux Distribution\n\nAuto-replaced with detected distribution:\n\n```\nhttps://example.com/{DIST}.png\n```\n\nBecomes: `debian.png`, `ubuntu.png`, `arch.png`, etc.\n\n### {BRIGHT} - Theme\n\nAuto-replaced with current theme:\n\n```\nhttps://example.com/{BRIGHT}.png\n```\n\nBecomes: `light.png` or `dark.png`\n\n### Combine Both\n\n```\nhttps://example.com/{DIST}-{BRIGHT}.png\n```\n\nBecomes: `debian-light.png`, `ubuntu-dark.png`, etc.\n\n## Tips\n\n- Use PNG or SVG formats\n- Recommended size: 64x64 to 128x128 pixels\n- Use HTTPS URLs\n- Keep file sizes small\n\n## Supported Distributions\n\ndebian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt\n\nFull list: [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)\n"
  },
  {
    "path": "docs/src/content/docs/advanced/json-settings.md",
    "content": "---\ntitle: Hidden Settings (JSON)\ndescription: Access advanced settings via JSON editor\n---\n\nSome settings are hidden from the UI but accessible via JSON editor.\n\n## Access\n\nLong-press **Settings** in drawer to open JSON editor.\n\n## Common Hidden Settings\n\n### timeOut\n\nConnection timeout in seconds.\n\n```json\n{\"timeOut\": 10}\n```\n\n**Type:** integer | **Default:** 5 | **Range:** 1-60\n\n### recordHistory\n\nSave history (SFTP paths, etc.).\n\n```json\n{\"recordHistory\": true}\n```\n\n**Type:** boolean | **Default:** true\n\n### textFactor\n\nText scaling factor.\n\n```json\n{\"textFactor\": 1.2}\n```\n\n**Type:** double | **Default:** 1.0 | **Range:** 0.8-1.5\n\n## Finding More Settings\n\nAll settings defined in [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart).\n\nLook for:\n```dart\nlate final settingName = StoreProperty(box, 'settingKey', defaultValue);\n```\n\n## ⚠️ Important\n\n**Before editing:**\n- **Create backup** - Wrong settings can cause app to not open\n- **Edit carefully** - JSON must be valid\n- **Change one at a time** - Test each setting\n\n## Recovery\n\nIf app won't open after editing:\n1. Clear app data (last resort)\n2. Reinstall app\n3. Restore from backup\n"
  },
  {
    "path": "docs/src/content/docs/advanced/troubleshooting.md",
    "content": "---\ntitle: Common Issues\ndescription: Solutions to common problems\n---\n\n## Connection Issues\n\n### SSH Won't Connect\n\n**Symptoms:** Timeout, connection refused, auth failed\n\n**Solutions:**\n\n1. **Verify server type:** Only Unix-like systems supported (Linux, macOS, Android/Termux)\n2. **Test manually:** `ssh user@server -p port`\n3. **Check firewall:** Port 22 must be open\n4. **Verify credentials:** Username and password/key correct\n\n### Frequent Disconnections\n\n**Symptoms:** Terminal disconnects after inactivity\n\n**Solutions:**\n\n1. **Server keep-alive:**\n   ```bash\n   # /etc/ssh/sshd_config\n   ClientAliveInterval 60\n   ClientAliveCountMax 3\n   ```\n\n2. **Disable battery optimization:**\n   - MIUI: Battery → \"No limits\"\n   - Android: Settings → Apps → Disable optimization\n   - iOS: Enable background refresh\n\n## Input Issues\n\n### Can't Type Certain Characters\n\n**Solution:** Settings → Keyboard Type → Switch to `visiblePassword`\n\nNote: CJK input may not work after this change.\n\n## App Issues\n\n### App Crashes on Startup\n\n**Symptoms:** App won't open, black screen\n\n**Causes:** Corrupted settings, especially from JSON editor\n\n**Solutions:**\n\n1. **Clear app data:**\n   - Android: Settings → Apps → ServerBox → Clear Data\n   - iOS: Delete and reinstall\n\n2. **Restore backup:** Import backup created before changing settings\n\n### Backup/Restore Issues\n\n**Backup not working:**\n- Check storage space\n- Verify app has storage permissions\n- Try different location\n\n**Restore fails:**\n- Verify backup file integrity\n- Check app version compatibility\n\n## Widget Issues\n\n### Widget Not Updating\n\n**iOS:**\n- Wait up to 30 minutes for automatic refresh\n- Remove and re-add widget\n- Check URL ends with `/status`\n\n**Android:**\n- Tap widget to force refresh\n- Verify widget ID matches configuration in app settings\n\n**watchOS:**\n- Restart watch app\n- Wait a few minutes after config change\n- Verify URL format\n\n### Widget Shows Error\n\n- Verify ServerBox Monitor is running on server\n- Test URL in browser\n- Check authentication credentials\n\n## Performance Issues\n\n### App is Slow\n\n**Solutions:**\n- Reduce refresh rate in settings\n- Check network speed\n- Disable unused servers\n\n### High Battery Usage\n\n**Solutions:**\n- Increase refresh intervals\n- Disable background refresh\n- Close unused SSH sessions\n\n## Getting Help\n\nIf issues persist:\n\n1. **Search GitHub Issues:** https://github.com/lollipopkit/flutter_server_box/issues\n2. **Create New Issue:** Include app version, platform, and steps to reproduce\n3. **Check Wiki:** This documentation and GitHub Wiki\n"
  },
  {
    "path": "docs/src/content/docs/advanced/widgets.md",
    "content": "---\ntitle: Home Screen Widgets\ndescription: Add server status widgets to your home screen\n---\n\nRequires [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) installed on your servers.\n\n## Prerequisites\n\nInstall ServerBox Monitor on your server first. See [ServerBox Monitor Wiki](https://github.com/lollipopkit/server_box_monitor/wiki/Home) for setup instructions.\n\nAfter installation, your server should have:\n- HTTP/HTTPS endpoint\n- `/status` API endpoint\n- Optional authentication\n\n## URL Format\n\n```\nhttps://your-server.com/status\n```\n\nMust end with `/status`.\n\n## iOS Widget\n\n### Setup\n\n1. Long press home screen → Tap **+**\n2. Search \"ServerBox\"\n3. Choose widget size\n4. Long press widget → **Edit Widget**\n5. Enter URL ending with `/status`\n\n### Notes\n\n- Must use HTTPS (except local IPs)\n- Max refresh rate: 30 minutes (iOS limit)\n- Add multiple widgets for multiple servers\n\n## Android Widget\n\n### Setup\n\n1. Long press home screen → **Widgets**\n2. Find \"ServerBox\" → Add to home screen\n3. Note the widget ID number displayed\n4. Open ServerBox app → Settings\n5. Tap **Config home widget link**\n6. Add entry: `Widget ID` = `Status URL`\n\nExample:\n- Key: `17`\n- Value: `https://my-server.com/status`\n\n7. Tap widget on home screen to refresh\n\n## watchOS Widget\n\n### Setup\n\n1. Open iPhone app → Settings\n2. **iOS Settings** → **Watch app**\n3. Tap **Add URL**\n4. Enter URL ending with `/status`\n5. Wait for watch app to sync\n\n### Notes\n\n- Try restarting watch app if not updating\n- Verify phone and watch are connected\n\n\n## Troubleshooting\n\n### Widget Not Updating\n\n**iOS:** Wait up to 30 minutes, then remove and re-add\n**Android:** Tap widget to force refresh, verify ID in settings\n**watchOS:** Restart watch app, wait a few minutes\n\n### Widget Shows Error\n\n- Verify ServerBox Monitor is running\n- Test URL in browser\n- Check URL ends with `/status`\n\n## Security\n\n- **Always use HTTPS** when possible\n- **Local IPs only** on trusted networks\n"
  },
  {
    "path": "docs/src/content/docs/de/advanced/bulk-import.md",
    "content": "---\ntitle: Massenimport von Servern\ndescription: Importieren Sie mehrere Server aus einer JSON-Datei\n---\n\nImportieren Sie mehrere Serverkonfigurationen gleichzeitig mithilfe einer JSON-Datei.\n\n## JSON-Format\n\n:::danger[Sicherheitswarnung]\n**Speichern Sie niemals Klartext-Passwörter in Dateien!** Dieses JSON-Beispiel zeigt ein Passwort-Feld nur zur Demonstration, aber Sie sollten:\n\n- **SSH-Schlüssel bevorzugen** (`keyId`) anstelle von `pwd` - diese sind sicherer\n- **Passwort-Manager** oder Umgebungsvariablen verwenden, wenn Sie Passwörter verwenden müssen\n- **Löschen Sie die Datei sofort** nach dem Import - lassen Sie keine Anmeldedaten herumliegen\n- **Fügen Sie sie zur .gitignore hinzu** - checken Sie niemals Anmeldedatendateien in die Versionsverwaltung ein\n:::\n\n```json\n[\n  {\n    \"name\": \"Mein Server\",\n    \"ip\": \"example.com\",\n    \"port\": 22,\n    \"user\": \"root\",\n    \"pwd\": \"password\",\n    \"keyId\": \"\",\n    \"tags\": [\"production\"],\n    \"autoConnect\": false\n  }\n]\n```\n\n## Felder\n\n| Feld | Erforderlich | Beschreibung |\n|-------|----------|-------------|\n| `name` | Ja | Anzeigename |\n| `ip` | Ja | Domain oder IP-Adresse |\n| `port` | Ja | SSH-Port (normalerweise 22) |\n| `user` | Ja | SSH-Benutzername |\n| `pwd` | Nein | Passwort (vermeiden - stattdessen SSH-Schlüssel verwenden) |\n| `keyId` | Nein | SSH-Schlüsselname (aus Private Keys - empfohlen) |\n| `tags` | Nein | Organisations-Tags |\n| `autoConnect` | Nein | Automatische Verbindung beim Start |\n\n## Import-Schritte\n\n1. Erstellen Sie eine JSON-Datei mit Serverkonfigurationen\n2. Einstellungen → Backup → Server massenhaft importieren\n3. Wählen Sie Ihre JSON-Datei aus\n4. Bestätigen Sie den Import\n\n## Beispiel\n\n```json\n[\n  {\n    \"name\": \"Produktion\",\n    \"ip\": \"prod.example.com\",\n    \"port\": 22,\n    \"user\": \"admin\",\n    \"keyId\": \"my-key\",\n    \"tags\": [\"production\", \"web\"]\n  },\n  {\n    \"name\": \"Entwicklung\",\n    \"ip\": \"dev.example.com\",\n    \"port\": 2222,\n    \"user\": \"dev\",\n    \"keyId\": \"dev-key\",\n    \"tags\": [\"development\"]\n  }\n]\n```\n\n## Tipps\n\n- **Verwenden Sie SSH-Schlüssel** anstelle von Passwörtern, wann immer möglich\n- **Testen Sie die Verbindung** nach dem Import\n- **Organisieren Sie mit Tags** für eine einfachere Verwaltung\n- **Löschen Sie die JSON-Datei** nach dem Import\n- **Checken Sie niemals** JSON-Dateien mit Anmeldedaten in die Versionsverwaltung ein\n"
  },
  {
    "path": "docs/src/content/docs/de/advanced/custom-commands.md",
    "content": "---\ntitle: Benutzerdefinierte Befehle\ndescription: Anzeige der Ausgabe benutzerdefinierter Befehle auf der Serverseite\n---\n\nFügen Sie benutzerdefinierte Shell-Befehle hinzu, um deren Ausgabe auf der Server-Detailseite anzuzeigen.\n\n## Einrichtung\n\n1. Servereinstellungen → Benutzerdefinierte Befehle\n2. Befehle im JSON-Format eingeben\n\n## Basisformat\n\n```json\n{\n  \"Anzeigename\": \"Shell-Befehl\"\n}\n```\n\n**Beispiel:**\n```json\n{\n  \"Speicher\": \"free -h\",\n  \"Festplatte\": \"df -h\",\n  \"Laufzeit\": \"uptime\"\n}\n```\n\n## Ergebnisse anzeigen\n\nNach der Einrichtung erscheinen benutzerdefinierte Befehle auf der Server-Detailseite und werden automatisch aktualisiert.\n\n## Spezielle Befehlsnamen\n\n### server_card_top_right\n\nAnzeige auf der Serverkarte der Startseite (oben rechts):\n\n```json\n{\n  \"server_card_top_right\": \"Ihr-Befehl-hier\"\n}\n```\n\n## Tipps\n\n**Absolute Pfade verwenden:**\n```json\n{\"Mein Skript\": \"/usr/local/bin/mein-skript.sh\"}\n```\n\n**Pipe-Befehle:**\n```json\n{\"Top-Prozess\": \"ps aux | sort -rk 3 | head -5\"}\n```\n\n**Ausgabe formatieren:**\n```json\n{\"CPU-Last\": \"uptime | awk -F'load average:' '{print $2}'\"}\n```\n\n**Befehle schnell halten:** Unter 5 Sekunden für das beste Erlebnis.\n\n**Ausgabe begrenzen:**\n```json\n{\"Logs\": \"tail -20 /var/log/syslog\"}\n```\n\n## Sicherheit\n\nBefehle werden mit den Berechtigungen des SSH-Benutzers ausgeführt. Vermeiden Sie Befehle, die den Systemzustand ändern.\n"
  },
  {
    "path": "docs/src/content/docs/de/advanced/custom-logo.md",
    "content": "---\ntitle: Benutzerdefiniertes Server-Logo\ndescription: Verwenden Sie benutzerdefinierte Bilder für Serverkarten\n---\n\nZeigen Sie benutzerdefinierte Logos auf Serverkarten mithilfe von Bild-URLs an.\n\n## Einrichtung\n\n1. Servereinstellungen → Benutzerdefiniertes Logo\n2. Bild-URL eingeben\n\n## URL-Platzhalter\n\n### {DIST} - Linux-Distribution\n\nWird automatisch durch die erkannte Distribution ersetzt:\n\n```\nhttps://example.com/{DIST}.png\n```\n\nWird zu: `debian.png`, `ubuntu.png`, `arch.png`, usw.\n\n### {BRIGHT} - Theme\n\nWird automatisch durch das aktuelle Theme ersetzt:\n\n```\nhttps://example.com/{BRIGHT}.png\n```\n\nWird zu: `light.png` oder `dark.png`\n\n### Beide kombinieren\n\n```\nhttps://example.com/{DIST}-{BRIGHT}.png\n```\n\nWird zu: `debian-light.png`, `ubuntu-dark.png`, usw.\n\n## Tipps\n\n- Verwenden Sie PNG- oder SVG-Formate\n- Empfohlene Größe: 64x64 bis 128x128 Pixel\n- Verwenden Sie HTTPS-URLs\n- Halten Sie die Dateigrößen gering\n\n## Unterstützte Distributionen\n\ndebian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt\n\nVollständige Liste: [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)\n"
  },
  {
    "path": "docs/src/content/docs/de/advanced/json-settings.md",
    "content": "---\ntitle: Versteckte Einstellungen (JSON)\ndescription: Zugriff auf erweiterte Einstellungen über den JSON-Editor\n---\n\nEinige Einstellungen sind in der Benutzeroberfläche ausgeblendet, aber über den JSON-Editor zugänglich.\n\n## Zugriff\n\nHalten Sie **Einstellungen** in der Seitenleiste lange gedrückt, um den JSON-Editor zu öffnen.\n\n## Gängige versteckte Einstellungen\n\n### timeOut\n\nVerbindungs-Timeout in Sekunden.\n\n```json\n{\"timeOut\": 10}\n```\n\n**Typ:** Integer | **Standard:** 5 | **Bereich:** 1-60\n\n### recordHistory\n\nVerlauf speichern (SFTP-Pfade, usw.).\n\n```json\n{\"recordHistory\": true}\n```\n\n**Typ:** Boolean | **Standard:** true\n\n### textFactor\n\nTextskalierungsfaktor.\n\n```json\n{\"textFactor\": 1.2}\n```\n\n**Typ:** Double | **Standard:** 1.0 | **Bereich:** 0.8-1.5\n\n## Weitere Einstellungen finden\n\nAlle Einstellungen sind in [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart) definiert.\n\nSuchen Sie nach:\n```dart\nlate final settingName = StoreProperty(box, 'settingKey', defaultValue);\n```\n\n## ⚠️ Wichtig\n\n**Vor dem Bearbeiten:**\n- **Backup erstellen** - Falsche Einstellungen können dazu führen, dass die App nicht mehr öffnet\n- **Sorgfältig bearbeiten** - JSON muss gültig sein\n\n## Wiederherstellung\n\nWenn die App nach dem Bearbeiten nicht mehr öffnet:\n1. App-Daten löschen (letzter Ausweg)\n2. App neu installieren\n3. Aus Backup wiederherstellen\n"
  },
  {
    "path": "docs/src/content/docs/de/advanced/troubleshooting.md",
    "content": "---\ntitle: Häufige Probleme\ndescription: Lösungen für gängige Probleme\n---\n\n## Verbindungsprobleme\n\n### SSH verbindet nicht\n\n**Symptome:** Timeout, Verbindung abgelehnt, Authentifizierung fehlgeschlagen\n\n**Lösungen:**\n\n1. **Servertyp überprüfen:** Nur Unix-ähnliche Systeme werden unterstützt (Linux, macOS, Android/Termux)\n2. **Manuell testen:** `ssh benutzer@server -p port`\n3. **Firewall prüfen:** Port 22 muss offen sein\n4. **Anmeldedaten prüfen:** Benutzername und Passwort/Schlüssel korrekt\n\n### Häufige Verbindungsabbrüche\n\n**Symptome:** Das Terminal trennt die Verbindung nach Inaktivität\n\n**Lösungen:**\n\n1. **Server Keep-Alive:**\n   ```bash\n   # /etc/ssh/sshd_config\n   ClientAliveInterval 60\n   ClientAliveCountMax 3\n   ```\n\n2. **Akku-Optimierung deaktivieren:**\n   - MIUI: Akku → \"Keine Beschränkungen\"\n   - Android: Einstellungen → Apps → Optimierung deaktivieren\n   - iOS: Hintergrundaktualisierung aktivieren\n\n## Eingabeprobleme\n\n### Bestimmte Zeichen können nicht getippt werden\n\n**Lösung:** Einstellungen → Tastaturtyp → Wechseln zu `visiblePassword`\n\nHinweis: CJK-Eingaben funktionieren nach dieser Änderung möglicherweise nicht mehr.\n\n## App-Probleme\n\n### App stürzt beim Start ab\n\n**Symptome:** App öffnet sich nicht, schwarzer Bildschirm\n\n**Ursachen:** Korrupte Einstellungen, insbesondere durch den JSON-Editor\n\n**Lösungen:**\n\n1. **App-Daten löschen:**\n   - Android: Einstellungen → Apps → ServerBox → Daten löschen\n   - iOS: Löschen und neu installieren\n\n2. **Backup wiederherstellen:** Importieren Sie ein Backup, das vor der Änderung der Einstellungen erstellt wurde\n\n### Probleme beim Sichern/Wiederherstellen\n\n**Backup funktioniert nicht:**\n- Speicherplatz prüfen\n- Sicherstellen, dass die App Speicherberechtigungen hat\n- Anderen Speicherort versuchen\n\n**Wiederherstellung schlägt fehl:**\n- Integrität der Backup-Datei prüfen\n- Kompatibilität der App-Version prüfen\n\n## Widget-Probleme\n\n### Widget aktualisiert nicht\n\n**iOS:**\n- Bis zu 30 Minuten auf automatische Aktualisierung warten\n- Widget entfernen und neu hinzufügen\n- Prüfen, ob die URL auf `/status` endet\n\n**Android:**\n- Auf das Widget tippen, um die Aktualisierung zu erzwingen\n- Sicherstellen, dass die Widget-ID mit der Konfiguration in den App-Einstellungen übereinstimmt\n\n**watchOS:**\n- Watch-App neu starten\n- Nach Konfigurationsänderung einige Minuten warten\n- URL-Format prüfen\n\n### Widget zeigt Fehler\n\n- Sicherstellen, dass der ServerBox Monitor auf dem Server läuft\n- URL im Browser testen\n- Authentifizierungsdaten prüfen\n\n## Leistungsprobleme\n\n### App ist langsam\n\n**Lösungen:**\n- Aktualisierungsrate in den Einstellungen reduzieren\n- Netzwerkgeschwindigkeit prüfen\n- Nicht verwendete Server deaktivieren\n\n### Hoher Akkuverbrauch\n\n**Lösungen:**\n- Aktualisierungsintervalle vergrößern\n- Hintergrundaktualisierung deaktivieren\n- Nicht verwendete SSH-Sitzungen schließen\n\n## Hilfe erhalten\n\nWenn die Probleme weiterhin bestehen:\n\n1. **GitHub Issues durchsuchen:** https://github.com/lollipopkit/flutter_server_box/issues\n2. **Neues Issue erstellen:** App-Version, Plattform und Schritte zur Reproduktion angeben\n3. **Wiki prüfen:** Diese Dokumentation und das GitHub Wiki\n"
  },
  {
    "path": "docs/src/content/docs/de/advanced/widgets.md",
    "content": "---\ntitle: Startbildschirm-Widgets\ndescription: Fügen Sie Serverstatus-Widgets zu Ihrem Startbildschirm hinzu\n---\n\nErfordert [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) auf Ihren Servern installiert.\n\n## Voraussetzungen\n\nInstallieren Sie zuerst ServerBox Monitor auf Ihrem Server. Anweisungen zur Einrichtung finden Sie im [ServerBox Monitor Wiki](https://github.com/lollipopkit/server_box_monitor/wiki/Home).\n\nNach der Installation sollte Ihr Server verfügen über:\n- Einen HTTP/HTTPS-Endpunkt\n- Einen `/status` API-Endpunkt\n- Optionale Authentifizierung\n\n## URL-Format\n\n```\nhttps://ihr-server.com/status\n```\n\nMuss auf `/status` enden.\n\n## iOS-Widget\n\n### Einrichtung\n\n1. Startbildschirm lange drücken → Auf **+** tippen\n2. Nach \"ServerBox\" suchen\n3. Widget-Größe wählen\n4. Widget lange drücken → **Widget bearbeiten**\n5. URL eingeben, die auf `/status` endet\n\n### Hinweise\n\n- Muss HTTPS verwenden (außer bei lokalen IPs)\n- Maximale Aktualisierungsrate: 30 Minuten (iOS-Limit)\n- Fügen Sie mehrere Widgets für mehrere Server hinzu\n\n## Android-Widget\n\n### Einrichtung\n\n1. Startbildschirm lange drücken → **Widgets**\n2. \"ServerBox\" finden → Zum Startbildschirm hinzufügen\n3. Notieren Sie sich die angezeigte Widget-ID-Nummer\n4. ServerBox-App öffnen → Einstellungen\n5. Tippen Sie auf **Config home widget link**\n6. Eintrag hinzufügen: `Widget ID` = `Status-URL`\n\nBeispiel:\n- Key: `17`\n- Value: `https://mein-server.com/status`\n\n7. Tippen Sie auf das Widget auf dem Startbildschirm, um es zu aktualisieren\n\n## watchOS-Widget\n\n### Einrichtung\n\n1. iPhone-App öffnen → Einstellungen\n2. **iOS-Einstellungen** → **Watch-App**\n3. Auf **URL hinzufügen** tippen\n4. URL eingeben, die auf `/status` endet\n5. Warten, bis die Watch-App synchronisiert ist\n\n### Hinweise\n\n- Versuchen Sie, die Watch-App neu zu starten, wenn sie nicht aktualisiert wird\n- Sicherstellen, dass Telefon und Watch verbunden sind\n\n## Fehlerbehebung\n\n### Widget aktualisiert nicht\n\n**iOS:** Warten Sie bis zu 30 Minuten, dann entfernen Sie es und fügen es erneut hinzu.\n**Android:** Tippen Sie auf das Widget, um die Aktualisierung zu erzwingen, überprüfen Sie die ID in den Einstellungen.\n**watchOS:** Starten Sie die Watch-App neu, warten Sie einige Minuten.\n\n### Widget zeigt Fehler an\n\n- Sicherstellen, dass ServerBox Monitor läuft\n- URL im Browser testen\n- Prüfen, ob die URL auf `/status` endet\n\n## Sicherheit\n\n- **Verwenden Sie immer HTTPS**, wann immer möglich\n- **Lokale IPs nur** in vertrauenswürdigen Netzwerken\n"
  },
  {
    "path": "docs/src/content/docs/de/development/architecture.md",
    "content": "---\ntitle: Architektur\ndescription: Architekturmuster und Designentscheidungen\n---\n\nServer Box folgt den Prinzipien der Clean Architecture mit einer klaren Trennung zwischen Daten-, Domänen- und Präsentationsschicht.\n\n## Schichtarchitektur\n\n```\n┌─────────────────────────────────────┐\n│          Präsentationsschicht       │\n│         (lib/view/page/)            │\n│  - Seiten, Widgets, Controller      │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│         Business-Logik-Schicht     │\n│      (lib/data/provider/)           │\n│  - Riverpod Provider               │\n│  - Zustandsverwaltung              │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│           Datenschicht              │\n│      (lib/data/model/, store/)      │\n│  - Modelle, Speicher, Dienste       │\n└─────────────────────────────────────┘\n```\n\n## Schlüsselmuster\n\n### Zustandsverwaltung: Riverpod\n\n- **Codegenerierung**: Verwendet `riverpod_generator` für typsichere Provider\n- **State Notifier**: Für veränderlichen Zustand mit Business-Logik\n- **Async Notifier**: Für Lade- und Fehlerzustände\n- **Stream Provider**: Für Echtzeitdaten\n\n### Unveränderliche Modelle: Freezed\n\n- Alle Datenmodelle verwenden Freezed für Unveränderlichkeit\n- Union-Typen zur Darstellung von Zuständen\n- Integrierte JSON-Serialisierung\n- CopyWith-Erweiterungen für Aktualisierungen\n\n### Lokale Speicherung: Hive\n\n- **hive_ce**: Community-Edition von Hive\n- Keine manuellen `@HiveField` oder `@HiveType` erforderlich\n- Typ-Adapter werden automatisch generiert\n- Persistenter Key-Value-Speicher\n\n## Dependency Injection\n\nDienste und Stores werden injiziert über:\n\n1. **Provider**: Stellen Abhängigkeiten der UI zur Verfügung\n2. **GetIt**: Service-Locator (wo anwendbar)\n3. **Konstruktor-Injektion**: Explizite Abhängigkeiten\n\n## Datenfluss\n\n```\nBenutzeraktion → Widget → Provider → Dienst/Store → Modell-Update → UI-Neuaufbau\n```\n\n1. Benutzer interagiert mit Widget\n2. Widget ruft Provider-Methode auf\n3. Provider aktualisiert Zustand über Dienst/Store\n4. Zustandsänderung löst Neuaufbau der UI aus\n5. Neuer Zustand spiegelt sich im Widget wider\n\n## Eigene Abhängigkeiten\n\nDas Projekt verwendet mehrere eigene Forks zur Funktionserweiterung:\n\n- **dartssh2**: Erweiterte SSH-Funktionen\n- **xterm**: Terminal-Emulator mit mobiler Unterstützung\n- **fl_lib**: Gemeinsame UI-Komponenten und Dienstprogramme\n\n## Threading\n\n- **Isolates**: Rechenintensive Aufgaben außerhalb des Main-Threads\n- **computer-Paket**: Dienstprogramme für Multi-Threading\n- **Async/Await**: Nicht-blockierende I/O-Operationen\n"
  },
  {
    "path": "docs/src/content/docs/de/development/building.md",
    "content": "---\ntitle: Bauen\ndescription: Bauanleitungen für verschiedene Plattformen\n---\n\nServer Box verwendet ein benutzerdefiniertes Build-System (`fl_build`) für plattformübergreifende Builds.\n\n## Voraussetzungen\n\n- Flutter SDK (stabiler Kanal)\n- Plattformspezifische Tools (Xcode für iOS, Android Studio für Android)\n- Rust-Toolchain (für einige native Abhängigkeiten)\n\n## Entwicklungs-Build\n\n```bash\n# Im Entwicklungsmodus ausführen\nflutter run\n\n# Auf einem bestimmten Gerät ausführen\nflutter run -d <device-id>\n```\n\n## Produktions-Build\n\nDas Projekt verwendet `fl_build` zum Bauen:\n\n```bash\n# Für eine bestimmte Plattform bauen\ndart run fl_build -p <platform>\n\n# Verfügbare Plattformen:\n# - ios\n# - android\n# - macos\n# - linux\n# - windows\n```\n\n## Plattformspezifische Builds\n\n### iOS\n\n```bash\ndart run fl_build -p ios\n```\n\nErfordert:\n- macOS mit Xcode\n- CocoaPods\n- Apple Developer Account für die Signierung\n\n### Android\n\n```bash\ndart run fl_build -p android\n```\n\nErfordert:\n- Android SDK\n- Java Development Kit\n- Keystore für die Signierung\n\n### macOS\n\n```bash\ndart run fl_build -p macos\n```\n\n### Linux\n\n```bash\ndart run fl_build -p linux\n```\n\n### Windows\n\n```bash\ndart run fl_build -p windows\n```\n\nErfordert Windows mit Visual Studio.\n\n## Vor/Nach dem Build\n\nDas Skript `make.dart` übernimmt:\n\n- Metadaten-Generierung\n- Aktualisierung der Versions-Strings\n- Plattformspezifische Konfigurationen\n\n## Fehlerbehebung\n\n### Clean Build\n\n```bash\nflutter clean\ndart run build_runner build --delete-conflicting-outputs\nflutter pub get\n```\n\n### Versions-Konflikt\n\nStellen Sie sicher, dass alle Abhängigkeiten kompatibel sind:\n```bash\nflutter pub upgrade\n```\n\n## Release-Checkliste\n\n1. Version in `pubspec.yaml` aktualisieren\n2. Codegenerierung ausführen\n3. Tests ausführen\n4. Für alle Zielplattformen bauen\n5. Auf physischen Geräten testen\n6. GitHub-Release erstellen\n"
  },
  {
    "path": "docs/src/content/docs/de/development/codegen.md",
    "content": "---\ntitle: Codegenerierung\ndescription: Verwendung von build_runner für die Codegenerierung\n---\n\nServer Box verwendet intensiv Codegenerierung für Modelle, Zustandsverwaltung und Serialisierung.\n\n## Wann sollte die Codegenerierung ausgeführt werden?\n\nFühren Sie sie aus nach der Änderung von:\n\n- Modellen mit `@freezed` Annotation\n- Klassen mit `@JsonSerializable`\n- Hive-Modellen\n- Providern mit `@riverpod`\n- Lokalisierungen (ARB-Dateien)\n\n## Codegenerierung ausführen\n\n```bash\n# Gesamten Code generieren\ndart run build_runner build --delete-conflicting-outputs\n\n# Bereinigen und neu generieren\ndart run build_runner build --delete-conflicting-outputs --clean\n```\n\n## Generierte Dateien\n\n### Freezed (`*.freezed.dart`)\n\nUnveränderliche Datenmodelle mit Union Types:\n\n```dart\n@freezed\nclass ServerState with _$ServerState {\n  const factory ServerState.connected() = Connected;\n  const factory ServerState.disconnected() = Disconnected;\n  const factory ServerState.error(String message) = Error;\n}\n```\n\n### JSON-Serialisierung (`*.g.dart`)\n\nGeneriert durch `json_serializable`:\n\n```dart\n@JsonSerializable()\nclass Server {\n  final String id;\n  final String name;\n  final String host;\n\n  Server({required this.id, required this.name, required this.host});\n\n  factory Server.fromJson(Map<String, dynamic> json) =>\n      _$ServerFromJson(json);\n  Map<String, dynamic> toJson() => _$ServerToJson(this);\n}\n```\n\n### Riverpod Provider (`*.g.dart`)\n\nGeneriert aus der `@riverpod` Annotation:\n\n```dart\n@riverpod\nclass MyNotifier extends _$MyNotifier {\n  @override\n  int build() => 0;\n}\n```\n\n### Hive-Adapter (`*.g.dart`)\n\nAutomatisch generiert für Hive-Modelle (hive_ce):\n\n```dart\n@HiveType(typeId: 0)\nclass ServerModel {\n  @HiveField(0)\n  final String id;\n}\n```\n\n## Generierung der Lokalisierung\n\n```bash\nflutter gen-l10n\n```\n\nGeneriert `lib/generated/l10n/` aus `lib/l10n/*.arb` Dateien.\n\n## Tipps\n\n- Verwenden Sie `--delete-conflicting-outputs`, um Konflikte zu vermeiden.\n- Fügen Sie generierte Dateien zur `.gitignore` hinzu.\n- Bearbeiten Sie generierte Dateien niemals manuell.\n"
  },
  {
    "path": "docs/src/content/docs/de/development/state.md",
    "content": "---\ntitle: Zustandsverwaltung\ndescription: Riverpod-basierte Zustandsverwaltungsmuster\n---\n\nServer Box verwendet Riverpod mit Codegenerierung für die Zustandsverwaltung.\n\n## Provider-Typen\n\n### StateProvider\n\nEinfacher Zustand, der gelesen und geschrieben werden kann:\n\n```dart\n@riverpod\nclass Settings extends _$Settings {\n  @override\n  SettingsModel build() {\n    return SettingsModel.defaults();\n  }\n\n  void update(SettingsModel newSettings) {\n    state = newSettings;\n  }\n}\n```\n\n### AsyncNotifierProvider\n\nZustand, der asynchron mit Lade-/Fehlerzuständen geladen wird:\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    return fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() => fetchStatus(server));\n  }\n}\n```\n\n### StreamProvider\n\nEchtzeitdaten aus Streams:\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  return cpuService.monitor(server);\n}\n```\n\n## Zustandsmuster\n\n### Ladezustände\n\n```dart\nstate.when(\n  data: (data) => DataWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### Family Provider\n\nParametrisierte Provider:\n\n```dart\n@riverpod\nList<Container> containers(ContainersRef ref, Server server) {\n  return containerService.list(server);\n}\n```\n\n### Auto-Dispose\n\nProvider, die verworfen werden, wenn sie nicht mehr referenziert werden:\n\n```dart\n@Riverpod(keepAlive: false)\nclass TempState extends _$TempState {\n  // ...\n}\n```\n\n## Best Practices\n\n1. **Codegenerierung nutzen**: Immer die `@riverpod` Annotation verwenden.\n2. **Provider lokal platzieren**: In der Nähe der Widgets platzieren, die sie nutzen.\n3. **Singletons vermeiden**: Stattdessen Provider verwenden.\n4. **Korrekt schichten**: UI-Logik von Business-Logik getrennt halten.\n\n## Zustand in Widgets lesen\n\n```dart\nclass ServerWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final status = ref.watch(serverStatusProvider(server));\n    return status.when(...);\n  }\n}\n```\n\n## Zustand ändern\n\n```dart\nref.read(settingsProvider.notifier).update(newSettings);\n```\n"
  },
  {
    "path": "docs/src/content/docs/de/development/structure.md",
    "content": "---\ntitle: Projektstruktur\ndescription: Verständnis der Server Box Codebasis\n---\n\nDas Server Box-Projekt folgt einer modularen Architektur mit einer klaren Trennung der Belange.\n\n## Verzeichnisstruktur\n\n```\nlib/\n├── core/              # Kern-Dienstprogramme und Erweiterungen\n├── data/              # Datenschicht\n│   ├── model/         # Datenmodelle nach Funktionen\n│   ├── provider/      # Riverpod Provider\n│   └── store/         # Lokale Speicherung (Hive)\n├── view/              # UI-Schicht\n│   ├── page/          # Hauptseiten\n│   └── widget/        # Wiederverwendbare Widgets\n├── generated/         # Generierte Lokalisierung\n├── l10n/              # Lokalisierungs-ARB-Dateien\n└── hive/              # Hive-Adapter\n```\n\n## Kernschicht (`lib/core/`)\n\nEnthält Dienstprogramme, Erweiterungen und Routing-Konfiguration:\n\n- **Erweiterungen**: Dart-Erweiterungen für gängige Typen\n- **Routen**: App-Routing-Konfiguration\n- **Dienstprogramme**: Gemeinsame Hilfsfunktionen\n\n## Datenschicht (`lib/data/`)\n\n### Modelle (`lib/data/model/`)\n\nOrganisiert nach Funktionen:\n\n- `server/` - Server-Verbindung und Status-Modelle\n- `container/` - Docker-Container-Modelle\n- `ssh/` - SSH-Sitzungs-Modelle\n- `sftp/` - SFTP-Datei-Modelle\n- `app/` - App-spezifische Modelle\n\n### Provider (`lib/data/provider/`)\n\nRiverpod Provider für Dependency Injection und Zustandsverwaltung:\n\n- Server Provider\n- UI-Zustands-Provider\n- Service Provider\n\n### Stores (`lib/data/store/`)\n\nHive-basierte lokale Speicherung:\n\n- Server-Speicher\n- Einstellungs-Speicher\n- Cache-Speicher\n\n## UI-Schicht (`lib/view/`)\n\n### Seiten (`lib/view/page/`)\n\nHauptbildschirme der Anwendung:\n\n- `server/` - Server-Verwaltungsseiten\n- `ssh/` - SSH-Terminal-Seiten\n- `container/` - Container-Seiten\n- `setting/` - Einstellungsseiten\n- `storage/` - SFTP-Seiten\n- `snippet/` - Snippet-Seiten\n\n### Widgets (`lib/view/widget/`)\n\nWiederverwendbare UI-Komponenten:\n\n- Server-Karten\n- Status-Diagramme\n- Eingabe-Komponenten\n- Dialoge\n\n## Generierte Dateien\n\n- `lib/generated/l10n/` - Automatisch generierte Lokalisierung\n- `*.g.dart` - Generierter Code (json_serializable, freezed, hive, riverpod)\n- `*.freezed.dart` - Unveränderliche Freezed-Klassen\n\n## Verzeichnis \"packages\" (`/packages/`)\n\nEnthält eigene Forks von Abhängigkeiten:\n\n- `dartssh2/` - SSH-Bibliothek\n- `xterm/` - Terminal-Emulator\n- `fl_lib/` - Gemeinsame Dienstprogramme\n- `fl_build/` - Build-System\n"
  },
  {
    "path": "docs/src/content/docs/de/development/testing.md",
    "content": "---\ntitle: Testen\ndescription: Teststrategien und Ausführung von Tests\n---\n\n## Tests ausführen\n\n```bash\n# Alle Tests ausführen\nflutter test\n\n# Bestimmte Testdatei ausführen\nflutter test test/battery_test.dart\n\n# Mit Coverage ausführen\nflutter test --coverage\n```\n\n## Teststruktur\n\nTests befinden sich im Verzeichnis `test/` und spiegeln die Struktur von `lib/` wider:\n\n```\ntest/\n├── data/\n│   ├── model/\n│   └── provider/\n├── view/\n│   └── widget/\n└── test_helpers.dart\n```\n\n## Unit-Tests\n\nGeschäftslogik und Datenmodelle testen:\n\n```dart\ntest('sollte CPU-Prozentsatz berechnen', () {\n  final cpu = CpuModel(usage: 75.0);\n  expect(cpu.usagePercentage, '75%');\n});\n```\n\n## Widget-Tests\n\nUI-Komponenten testen:\n\n```dart\ntestWidgets('ServerCard zeigt Servernamen an', (tester) async {\n  await tester.pumpWidget(\n    ProviderScope(\n      child: MaterialApp(\n        home: ServerCard(server: testServer),\n      ),\n    ),\n  );\n\n  expect(find.text('Test Server'), findsOneWidget);\n});\n```\n\n## Provider-Tests\n\nRiverpod Provider testen:\n\n```dart\ntest('serverStatusProvider gibt Status zurück', () async {\n  final container = ProviderContainer();\n  final status = await container.read(serverStatusProvider(testServer).future);\n  expect(status, isA<StatusModel>());\n});\n```\n\n## Mocking\n\nMocks für externe Abhängigkeiten verwenden:\n\n```dart\nclass MockSshService extends Mock implements SshService {}\n\ntest('verbindet zum Server', () async {\n  final mockSsh = MockSshService();\n  when(mockSsh.connect(any)).thenAnswer((_) async => true);\n\n  // Test mit Mock\n});\n```\n\n## Integrationstests\n\nKomplette Benutzerabläufe testen (in `integration_test/`):\n\n```dart\ntestWidgets('Server hinzufügen Ablauf', (tester) async {\n  await tester.pumpWidget(MyApp());\n\n  // Hinzufügen-Button tippen\n  await tester.tap(find.byIcon(Icons.add));\n  await tester.pumpAndSettle();\n\n  // Formular ausfüllen\n  await tester.enterText(find.byKey(Key('name')), 'Test Server');\n  // ...\n});\n```\n\n## Best Practices\n\n1. **Arrange-Act-Assert**: Tests klar strukturieren\n2. **Beschreibende Namen**: Testnamen sollten das Verhalten beschreiben\n3. **Eine Assertion pro Test**: Tests fokussiert halten\n4. **Externe Abhängigkeiten mocken**: Nicht von echten Servern abhängig sein\n5. **Grenzfälle testen**: Leere Listen, Null-Werte, usw.\n"
  },
  {
    "path": "docs/src/content/docs/de/index.mdx",
    "content": "---\ntitle: Server Box\ndescription: Eine umfassende plattformübergreifende Server-Management-Anwendung\nhero:\n  tagline: Verwalten Sie Ihre Linux-Server von überall aus\n  actions:\n    - text: Loslegen\n      link: /de/introduction/\n      icon: right-arrow\n      variant: primary\n    - text: Auf GitHub ansehen\n      link: https://github.com/lollipopkit/flutter_server_box\n      icon: github\n      variant: minimal\n---\n\nimport { Card, CardGrid } from '@astrojs/starlight/components';\n\n## Funktionen\n\n<CardGrid stagger>\n\t<Card title=\"Echtzeit-Überwachung\" icon=\"chart\">\n\t\tÜberwachen Sie CPU, Arbeitsspeicher, Festplatte, Netzwerk, GPU und Temperatur mit ansprechenden Echtzeit-Diagrammen.\n\t</Card>\n\t<Card title=\"SSH-Terminal\" icon=\"terminal\">\n\t\tVoll ausgestattetes SSH-Terminal mit Multi-Tab-Unterstützung und virtueller Tastatur für mobile Geräte.\n\t</Card>\n\t<Card title=\"SFTP-Dateibrowser\" icon=\"folder\">\n\t\tVerwalten Sie Dateien auf Ihren Servern mit dem integrierten SFTP-Client und dem lokalen Dateibrowser.\n\t</Card>\n\t<Card title=\"Docker-Verwaltung\" icon=\"box\">\n\t\tStarten, stoppen und überwachen Sie Docker-Container mit einer intuitiven Benutzeroberfläche.\n\t</Card>\n\t<Card title=\"Plattformübergreifend\" icon=\"device-mobile\">\n\t\tVerfügbar für iOS, Android, macOS, Linux, Windows und watchOS.\n\t</Card>\n\t<Card title=\"12+ Sprachen\" icon=\"globe\">\n\t\tVollständige Lokalisierungsunterstützung inklusive Englisch, Chinesisch, Deutsch, Französisch und mehr.\n\t</Card>\n</CardGrid>\n\n## Quick-Links\n\n- **Download**: Verfügbar im [App Store](https://apps.apple.com/app/id1586449703), auf [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) und bei [F-Droid](https://f-droid.org/)\n- **Dokumentation**: Entdecken Sie die Anleitungen für den Einstieg in die Server Box\n- **Support**: Treten Sie unserer Community auf GitHub für Diskussionen und Probleme bei\n"
  },
  {
    "path": "docs/src/content/docs/de/installation.mdx",
    "content": "---\ntitle: Installation\ndescription: Laden Sie Server Box herunter und installieren Sie es auf Ihrem Gerät\n---\n\nServer Box ist für mehrere Plattformen verfügbar. Wählen Sie Ihre bevorzugte Installationsmethode.\n\n## Mobile Apps\n\n### iOS\n\nLaden Sie es aus dem **[App Store](https://apps.apple.com/app/id1586449703)** herunter.\n\n### Android\n\nWählen Sie Ihre bevorzugte Quelle:\n\n- **[F-Droid](https://f-droid.org/)** – Für Benutzer, die reine FOSS-Quellen bevorzugen\n- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** – Für die neueste Version direkt von der Quelle\n\n## Desktop Apps\n\n### macOS\n\nHerunterladen von den **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\nFunktionen:\n- Native Menüleisten-Integration\n- Unterstützung für sowohl Intel als auch Apple Silicon\n\n### Linux\n\nHerunterladen von den **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\nVerfügbar als AppImage, deb oder tar.gz Pakete.\n\n### Windows\n\nHerunterladen von den **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\n## watchOS\n\nVerfügbar im **[App Store](https://apps.apple.com/app/id1586449703)** als Teil der iOS-App.\n\n## Aus dem Quellcode bauen\n\nUm Server Box aus dem Quellcode zu bauen, lesen Sie den Abschnitt [Bauen](/de/development/building/) in der Entwicklungsdokumentation.\n\n## Versionsinformationen\n\nBesuchen Sie die Seite [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) für die neueste Version und das Änderungsprotokoll.\n"
  },
  {
    "path": "docs/src/content/docs/de/introduction.mdx",
    "content": "---\ntitle: Einführung\ndescription: Erfahren Sie, was Server Box ist und was es kann\n---\n\nServer Box ist eine umfassende plattformübergreifende Server-Management-Anwendung, die mit Flutter entwickelt wurde. Sie ermöglicht es Ihnen, Ihre Linux-, Unix- und Windows-Server von überall aus zu überwachen, zu verwalten und zu steuern.\n\n## Was ist Server Box?\n\nServer Box bietet eine einheitliche Oberfläche für Server-Administrationsaufgaben über SSH-Verbindungen. Egal, ob Sie Systemadministrator, Entwickler oder Hobbyist mit eigenen Heimservern sind – diese App bietet Ihnen leistungsstarke Server-Management-Tools direkt in Ihrer Tasche.\n\n## Kernfunktionen\n\n- **Echtzeit-Überwachung**: Verfolgen Sie CPU, Arbeitsspeicher, Festplattenbelegung, Netzwerkgeschwindigkeit, GPU-Status und Systemtemperaturen.\n- **SSH-Terminal**: Voller Terminalzugriff mit Multi-Tab-Unterstützung und anpassbarem Erscheinungsbild.\n- **SFTP-Client**: Durchsuchen und verwalten Sie Dateien auf Ihren Servern.\n- **Docker-Verwaltung**: Steuern Sie Container mit Leichtigkeit.\n- **Prozess-Management**: Systemprozesse anzeigen und verwalten.\n- **Systemd-Dienste**: Systemd-Dienste starten, stoppen und überwachen.\n- **Netzwerk-Tools**: iPerf-Tests, Ping und Wake-on-LAN.\n- **Snippets**: Benutzerdefinierte Shell-Befehle speichern und ausführen.\n\n## Unterstützte Plattformen\n\nServer Box ist wahrhaft plattformübergreifend:\n\n- **Mobil**: iOS und Android\n- **Desktop**: macOS, Linux und Windows\n\n## Lizenz\n\nDieses Projekt ist unter der AGPL v3 lizenziert. Der Quellcode ist auf [GitHub](https://github.com/lollipopkit/flutter_server_box) verfügbar.\n"
  },
  {
    "path": "docs/src/content/docs/de/platforms/desktop.md",
    "content": "---\ntitle: Desktop-Funktionen\ndescription: Spezifische Funktionen für macOS, Linux und Windows\n---\n\nServer Box bietet auf Desktop-Plattformen zusätzliche Produktivitätsfunktionen.\n\n## macOS\n\n### Menüleisten-Integration\n\n- Schneller Serverstatus in der Menüleiste\n- Serverzugriff mit einem Klick\n- Kompaktmodus für minimale Ablenkung\n- Natives macOS-Menüleistenstyling\n\n### Beständigkeit des Fensterzustands\n\n- Merkt sich Fensterposition und -größe\n- Wiederherstellung der vorherigen Sitzung beim Start\n- Unterstützung für mehrere Monitore\n\n### Native Funktionen\n\n- **Titelleiste**: Option für benutzerdefinierte oder System-Titelleiste\n- **Vollbildmodus**: Dedizierte Serverüberwachung\n- **Tastenkombinationen**: macOS-native Tastenkürzel\n- **Touch Bar** (unterstützte Geräte): Schnellaktionen\n\n## Linux\n\n### Native Integration\n\n- Unterstützung für System-Tray\n- Integration von Desktop-Benachrichtigungen\n- Integration der Dateiauswahl\n\n### Fensterverwaltung\n\n- Unterstützung für X11 und Wayland\n- Freundlich gegenüber Tiling-Window-Managern\n- Option für benutzerdefinierte Fensterdekorationen\n\n## Windows\n\n### Funktionen\n\n- System-Tray-Integration\n- Jump List Schnellaktionen\n- Native Fenstersteuerung\n- Option für Autostart beim Booten\n\n## Plattformübergreifende Desktop-Funktionen\n\n### Tastenkombinationen\n\n- **Cmd/Ctrl + N**: Neuer Server\n- **Cmd/Ctrl + W**: Tab schließen\n- **Cmd/Ctrl + T**: Neuer Terminal-Tab\n- **Cmd/Ctrl + ,**: Einstellungen\n\n### Themes\n\n- Helles Theme\n- Dunkles Theme\n- AMOLED Theme (reines Schwarz)\n- System-Theme (folgt dem Betriebssystem)\n\n### Mehrere Fenster\n\n- Öffnen mehrerer Server in separaten Fenstern\n- Tabs in ein neues Fenster ziehen\n- Serverstatistiken Seite an Seite vergleichen\n\n### Vorteile gegenüber Mobile\n\n- Größerer Bildschirm für die Überwachung\n- Volle Tastatur für das Terminal\n- Schnellere Dateioperationen\n- Besseres Multitasking\n"
  },
  {
    "path": "docs/src/content/docs/de/platforms/mobile.md",
    "content": "---\ntitle: Mobile Funktionen\ndescription: Spezifische Funktionen für iOS und Android\n---\n\nServer Box bietet mehrere mobile-spezifische Funktionen für iOS- und Android-Geräte.\n\n## Biometrische Authentifizierung\n\nSichern Sie Ihre Server mit biometrischer Authentifizierung:\n\n- **iOS**: Face ID oder Touch ID\n- **Android**: Fingerabdruck-Authentifizierung\n\nAktivieren Sie dies unter Einstellungen > Sicherheit > Biometrische Authentifizierung.\n\n## Startbildschirm-Widgets\n\nFügen Sie Serverstatus-Widgets zu Ihrem Startbildschirm für eine schnelle Überwachung hinzu.\n\n### iOS\n\n- Auf den Startbildschirm lange drücken\n- Auf **+** tippen, um ein Widget hinzuzufügen\n- Nach \"Server Box\" suchen\n- Widget-Größe wählen:\n  - Klein: Status eines einzelnen Servers\n  - Mittel: Mehrere Server\n  - Groß: Detaillierte Informationen\n\n### Android\n\n- Auf den Startbildschirm lange drücken\n- Auf **Widgets** tippen\n- \"Server Box\" finden\n- Widget-Typ auswählen\n\n## Hintergrundbetrieb\n\n### Android\n\nVerbindungen im Hintergrund aufrechterhalten:\n\n- Aktivieren unter Einstellungen > Erweitert > Hintergrundbetrieb\n- Erfordert Ausschluss von der Akku-Optimierung\n- Permanente Benachrichtigungen für aktive Verbindungen\n\n### iOS\n\nEs gelten Hintergrundbeschränkungen:\n\n- Verbindungen können im Hintergrund pausieren\n- Schnelle Wiederverbindung bei Rückkehr zur App\n- Unterstützung für Hintergrundaktualisierung\n\n## Push-Benachrichtigungen\n\nErhalten Sie Benachrichtigungen für:\n\n- Server-Offline-Alarme\n- Warnungen bei hoher Ressourcenauslastung\n- Alarme bei Abschluss von Aufgaben\n\nKonfigurieren unter Einstellungen > Benachrichtigungen.\n\n## Mobile UI-Funktionen\n\n- **Pull to Refresh**: Serverstatus aktualisieren\n- **Wischgesten**: Schnelle Serveroperationen\n- **Querformat**: Besseres Terminal-Erlebnis\n- **Virtuelle Tastatur**: Terminal-Shortcuts\n\n## Datei-Integration\n\n- **Dateien-App (iOS)**: Direkter SFTP-Zugriff aus Dateien\n- **Storage Access Framework (Android)**: Dateien mit anderen Apps teilen\n- **Dokumentenauswahl**: Einfache Dateiauswahl\n"
  },
  {
    "path": "docs/src/content/docs/de/principles/architecture.md",
    "content": "---\ntitle: Architektur-Übersicht\ndescription: High-Level-Anwendungsarchitektur\n---\n\nServer Box folgt einer Schichtarchitektur mit klarer Trennung der Belange (Separation of Concerns).\n\n## Architektur-Schichten\n\n```\n┌─────────────────────────────────────────────────┐\n│          Präsentationsschicht (UI)             │\n│          lib/view/page/, lib/view/widget/       │\n│  - Seiten, Widgets, Controller                   │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│         Business-Logik-Schicht                 │\n│         lib/data/provider/                      │\n│  - Riverpod Provider, State Notifier            │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│           Datenzugriffsschicht                  │\n│         lib/data/store/, lib/data/model/        │\n│  - Hive Stores, Datenmodelle                     │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│         Externe Integrationsschicht             │\n│  - SSH (dartssh2), Terminal (xterm), SFTP       │\n│  - Plattformspezifischer Code (iOS, Android etc.)│\n└─────────────────────────────────────────────────┘\n```\n\n## Anwendungsgrundlagen\n\n### Haupteinstiegspunkt\n\n`lib/main.dart` initialisiert die App:\n\n```dart\nvoid main() {\n  runApp(\n    ProviderScope(\n      child: MyApp(),\n    ),\n  );\n}\n```\n\n### Root-Widget\n\n`MyApp` bietet:\n- **Theme-Management**: Umschalten zwischen hellem/dunklem Theme\n- **Routing-Konfiguration**: Navigationsstruktur\n- **Provider Scope**: Wurzel für Dependency Injection\n\n### Startseite\n\n`HomePage` dient als Navigationszentrum:\n- **Tab-Interface**: Server, Snippet, Container, SSH\n- **Zustandsverwaltung**: Zustand pro Tab\n- **Navigation**: Funktionszugriff\n\n## Kernsysteme\n\n### Zustandsverwaltung: Riverpod\n\n**Warum Riverpod?**\n- Sicherheit zur Kompilierzeit\n- Einfache Testbarkeit\n- Keine Abhängigkeit vom Build-Kontext\n- Funktioniert plattformübergreifend\n\n**Verwendete Provider-Typen:**\n- `StateProvider`: Einfacher veränderlicher Zustand\n- `AsyncNotifierProvider`: Lade-/Fehler-/Datenzustände\n- `StreamProvider`: Echtzeit-Datenströme\n- Future Provider: Einmalige asynchrone Operationen\n\n### Datenpersistenz: Hive CE\n\n**Warum Hive CE?**\n- Keine Abhängigkeiten von nativem Code\n- Schneller Key-Value-Speicher\n- Typsicher durch Codegenerierung\n- Keine manuellen Feld-Annotationen erforderlich\n\n**Stores:**\n- `SettingStore`: App-Einstellungen\n- `ServerStore`: Server-Konfigurationen\n- `SnippetStore`: Befehls-Snippets\n- `KeyStore`: SSH-Schlüssel\n\n### Immutable Modelle: Freezed\n\n**Vorteile:**\n- Unveränderlichkeit zur Kompilierzeit\n- Union Types für Zustände\n- Integrierte JSON-Serialisierung\n- CopyWith-Erweiterungen\n\n## Cross-Plattform-Strategie\n\n### Plugin-System\n\nFlutter-Plugins ermöglichen die Plattformintegration:\n\n| Plattform | Integrationsmethode |\n|-----------|--------------------|\n| iOS | CocoaPods, Swift/Obj-C |\n| Android | Gradle, Kotlin/Java |\n| macOS | CocoaPods, Swift |\n| Linux | CMake, C++ |\n| Windows | CMake, C# |\n\n### Plattformspezifische Funktionen\n\n**Nur iOS:**\n- Startbildschirm-Widgets\n- Live-Aktivitäten\n- Apple Watch Begleit-App\n\n**Nur Android:**\n- Hintergrunddienst\n- Push-Benachrichtigungen\n- Dateisystemzugriff\n\n**Nur Desktop:**\n- Menüleisten-Integration\n- Mehrere Fenster\n- Benutzerdefinierte Titelleiste\n\n## Eigene Abhängigkeiten\n\n### dartssh2 Fork\n\nErweiterter SSH-Client mit:\n- Besserer mobiler Unterstützung\n- Verbesserter Fehlerbehandlung\n- Leistungsoptimierungen\n\n### xterm.dart Fork\n\nTerminal-Emulator mit:\n- Für Mobilgeräte optimiertem Rendering\n- Unterstützung für Touch-Gesten\n- Integration der virtuellen Tastatur\n\n### fl_lib\n\nPaket mit gemeinsamen Dienstprogrammen:\n- Gemeinsame Widgets\n- Erweiterungen\n- Hilfsfunktionen\n\n## Build-System\n\n### fl_build Paket\n\nEigenes Build-System für:\n- Multi-Plattform-Builds\n- Code-Signierung\n- Asset-Bündelung\n- Versionsverwaltung\n\n### Build-Prozess\n\n```\nmake.dart (Version) → fl_build (Build) → Plattform-Output\n```\n\n1. **Pre-build**: Berechnung der Version aus Git\n2. **Build**: Kompilierung für die Zielplattform\n3. **Post-build**: Paketierung und Signierung\n\n## Beispiel für den Datenfluss\n\n### Aktualisierung des Serverstatus\n\n```\n1. Timer löst aus →\n2. Provider ruft Service auf →\n3. Service führt SSH-Befehl aus →\n4. Antwort wird in Modell geparst →\n5. Zustand wird aktualisiert →\n6. UI wird mit neuen Daten neu aufgebaut\n```\n\n### Ablauf einer Benutzeraktion\n\n```\n1. Benutzer tippt auf Schaltfläche →\n2. Widget ruft Provider-Methode auf →\n3. Provider aktualisiert Zustand →\n4. Zustandsänderung löst Neuaufbau aus →\n5. Neuer Zustand spiegelt sich in der UI wider\n```\n\n## Sicherheitsarchitektur\n\n### Datenschutz\n\n- **Passwörter**: Verschlüsselt mit flutter_secure_storage\n- **SSH-Schlüssel**: Verschlüsselt gespeichert\n- **Host-Fingerabdrücke**: Sicher gespeichert\n- **Sitzungsdaten**: Werden nicht persistiert\n\n### Verbindungssicherheit\n\n- **Host-Key-Verifizierung**: MITM-Erkennung\n- **Verschlüsselung**: Standard-SSH-Verschlüsselung\n- **Kein Klartext**: Sensible Daten werden niemals im Klartext gespeichert\n"
  },
  {
    "path": "docs/src/content/docs/de/principles/sftp.md",
    "content": "---\ntitle: SFTP-System\ndescription: Wie der SFTP-Dateibrowser funktioniert\n---\n\nDas SFTP-System bietet Dateimanagement-Funktionen über SSH.\n\n## Architektur\n\n```\n┌─────────────────────────────────────────────┐\n│              SFTP UI Schicht                │\n│  - Dateibrowser (remote)                    │\n│  - Dateibrowser (lokal)                     │\n│  - Transfer-Warteschlange                   │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          SFTP-Zustandsverwaltung            │\n│  - sftpProvider                             │\n│  - Pfadverwaltung                           │\n│  - Operations-Warteschlange                 │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         SFTP-Protokollschicht               │\n│  - SSH-Subsystem                            │\n│  - Dateioperationen                         │\n│  - Verzeichnisauflistung                    │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│            SSH-Transport                    │\n│  - Sicherer Kanal                           │\n│  - Daten-Streaming                          │\n└─────────────────────────────────────────────┘\n```\n\n## Verbindungsaufbau\n\n### Erstellung des SFTP-Clients\n\n```dart\nFuture<SftpClient> createSftpClient(Spi spi) async {\n  // 1. SSH-Client abrufen (wiederverwenden, falls verfügbar)\n  final sshClient = await genClient(spi);\n\n  // 2. SFTP-Subsystem öffnen\n  final sftp = await sshClient.openSftp();\n\n  return sftp;\n}\n```\n\n### Wiederverwendung von Verbindungen\n\nSFTP verwendet bestehende SSH-Verbindungen wieder:\n\n```dart\nclass ServerProvider {\n  SSHClient? _sshClient;\n  SftpClient? _sftpClient;\n\n  Future<SftpClient> getSftpClient(String spiId) async {\n    _sftpClient ??= await _sshClient!.openSftp();\n    return _sftpClient!;\n  }\n}\n```\n\n## Dateisystem-Operationen\n\n### Verzeichnisauflistung\n\n```dart\nFuture<List<SftpFile>> listDirectory(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Verzeichnis auflisten\n  final files = await sftp.listDir(path);\n\n  // Basierend auf Einstellungen sortieren\n  files.sort((a, b) {\n    switch (sortOption) {\n      case SortOption.name:\n        return a.name.toLowerCase().compareTo(b.name.toLowerCase());\n      case SortOption.size:\n        return a.size.compareTo(b.size);\n      case SortOption.time:\n        return a.modified.compareTo(b.modified);\n    }\n  });\n\n  // Ordner zuerst, falls aktiviert\n  if (showFoldersFirst) {\n    final dirs = files.where((f) => f.isDirectory);\n    final regular = files.where((f) => !f.isDirectory);\n    return [...dirs, ...regular];\n  }\n\n  return files;\n}\n```\n\n### Dateimetadaten\n\n```dart\nclass SftpFile {\n  final String name;\n  final String path;\n  final int size;           // Bytes\n  final int modified;       // Unix-Zeitstempel\n  final String permissions;  // z.B. \"rwxr-xr-x\"\n  final String owner;\n  final String group;\n  final bool isDirectory;\n  final bool isSymlink;\n\n  String get sizeFormatted => formatBytes(size);\n  String get modifiedFormatted => formatDate(modified);\n}\n```\n\n## Dateioperationen\n\n### Hochladen\n\n```dart\nFuture<void> uploadFile(\n  String localPath,\n  String remotePath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Anfrage erstellen\n  final req = SftpReq(\n    spi: spi,\n    remotePath: remotePath,\n    localPath: localPath,\n    type: SftpReqType.upload,\n  );\n\n  // Zur Warteschlange hinzufügen\n  _transferQueue.add(req);\n\n  // Transfer mit Fortschritt ausführen\n  final file = File(localPath);\n  final size = await file.length();\n  final stream = file.openRead();\n\n  await sftp.upload(\n    stream: stream,\n    toPath: remotePath,\n    onProgress: (transferred) {\n      _updateProgress(req, transferred, size);\n    },\n  );\n\n  // Fertigstellen\n  _transferQueue.remove(req);\n}\n```\n\n### Herunterladen\n\n```dart\nFuture<void> downloadFile(\n  String remotePath,\n  String localPath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Lokale Datei erstellen\n  final file = File(localPath);\n  final sink = file.openWrite();\n\n  // Herunterladen mit Fortschritt\n  final stat = await sftp.stat(remotePath);\n\n  await sftp.download(\n    fromPath: remotePath,\n    toSink: sink,\n    onProgress: (transferred) {\n      _updateProgress(\n        SftpReq(...),\n        transferred,\n        stat.size,\n      );\n    },\n  );\n\n  await sink.close();\n}\n```\n\n### Berechtigungen bearbeiten\n\n```dart\nFuture<void> setPermissions(\n  String path,\n  String permissions,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Berechtigungen parsen (z.B. \"rwxr-xr-x\" oder \"755\")\n  final mode = parsePermissions(permissions);\n\n  // Über SSH-Befehl setzen (zuverlässiger als SFTP)\n  final ssh = await getSshClient(spiId);\n  await ssh.exec('chmod $mode \"$path\"');\n}\n```\n\n## Pfadverwaltung\n\n### Pfadstruktur\n\n```dart\nclass PathWithPrefix {\n  final String prefix;  // z.B. \"/home/user\"\n  final String path;    // Relativ oder absolut\n\n  String get fullPath {\n    if (path.startsWith('/')) {\n      return path;  // Absoluter Pfad\n    }\n    return '$prefix/$path';  // Relativer Pfad\n  }\n\n  PathWithPrefix cd(String subPath) {\n    return PathWithPrefix(\n      prefix: fullPath,\n      path: subPath,\n    );\n  }\n}\n```\n\n### Navigationsverlauf\n\n```dart\nclass PathHistory {\n  final List<String> _history = [];\n  int _index = -1;\n\n  void push(String path) {\n    // Vorwärtsverlauf entfernen\n    _history.removeRange(_index + 1, _history.length);\n    _history.add(path);\n    _index = _history.length - 1;\n  }\n\n  String? back() {\n    if (_index > 0) {\n      _index--;\n      return _history[_index];\n    }\n    return null;\n  }\n\n  String? forward() {\n    if (_index < _history.length - 1) {\n      _index++;\n      return _history[_index];\n    }\n    return null;\n  }\n}\n```\n\n## Transfersystem\n\n### Transfer-Anfrage\n\n```dart\nclass SftpReq {\n  final Spi spi;\n  final String remotePath;\n  final String localPath;\n  final SftpReqType type;\n  final DateTime createdAt;\n\n  int? totalBytes;\n  int? transferredBytes;\n  String? error;\n}\n```\n\n### Fortschrittsverfolgung\n\n```dart\nclass TransferProgress {\n  final SftpReq request;\n  final int total;\n  final int transferred;\n  final DateTime startTime;\n\n  double get percentage => (transferred / total) * 100;\n  Duration get elapsed => DateTime.now().difference(startTime);\n\n  String get speedFormatted {\n    final bytesPerSecond = transferred / elapsed.inSeconds;\n    return formatSpeed(bytesPerSecond);\n  }\n}\n```\n\n### Warteschlangen-Management\n\n```dart\nclass TransferQueue {\n  final List<SftpReq> _queue = [];\n  final Map<String, TransferProgress> _progress = {};\n  int _concurrent = 3;  // Max. gleichzeitige Transfers\n\n  Future<void> process() async {\n    final active = _progress.values.where((p) => p.isInProgress);\n    if (active.length >= _concurrent) return;\n\n    final pending = _queue.where((r) => !_progress.containsKey(r.id));\n    for (final req in pending.take(_concurrent - active.length)) {\n      _executeTransfer(req);\n    }\n  }\n\n  Future<void> _executeTransfer(SftpReq req) async {\n    try {\n      _progress[req.id] = TransferProgress.inProgress(req);\n\n      if (req.type == SftpReqType.upload) {\n        await uploadFile(req.localPath, req.remotePath);\n      } else {\n        await downloadFile(req.remotePath, req.localPath);\n      }\n\n      _progress[req.id] = TransferProgress.completed(req);\n    } catch (e) {\n      _progress[req.id] = TransferProgress.failed(req, e);\n    }\n  }\n}\n```\n\n## Lokales Speichermuster\n\n### Download-Cache\n\nHeruntergeladene Dateien werden gespeichert unter:\n\n```dart\nString getLocalDownloadPath(String spiId, String remotePath) {\n  final normalized = remotePath.replaceAll('/', '_');\n  return 'Paths.file/$spiId/$normalized';\n}\n```\n\nBeispiel:\n- Remote: `/var/log/nginx/access.log`\n- spiId: `server-123`\n- Lokal: `Paths.file/server-123/_var_log_nginx_access.log`\n\n## Dateibearbeitung\n\n### Bearbeitungs-Workflow\n\n```dart\nFuture<void> editFile(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 1. Größe prüfen\n  final stat = await sftp.stat(path);\n  if (stat.size > editorMaxSize) {\n    showWarning('Datei zu groß für den integrierten Editor');\n    return;\n  }\n\n  // 2. Temporär herunterladen\n  final temp = await downloadToTemp(path);\n\n  // 3. Im Editor öffnen\n  final content = await openEditor(temp.path);\n\n  // 4. Zurück hochladen\n  await uploadFile(temp.path, path);\n\n  // 5. Aufräumen\n  await temp.delete();\n}\n```\n\n### Integration externer Editoren\n\n```dart\nFuture<void> editInExternalEditor(String path) async {\n  final ssh = await getSshClient(spiId);\n\n  // Terminal mit Editor öffnen\n  final editor = getSetting('sftpEditor', 'vim');\n  await ssh.exec('$editor \"$path\"');\n\n  // Benutzer bearbeitet im Terminal\n  // Nach dem Speichern die SFTP-Ansicht aktualisieren\n}\n```\n\n## Fehlerbehandlung\n\n### Berechtigungsfehler\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpPermissionException {\n  showError('Berechtigung verweigert: ${stat.path}');\n  showHint('Prüfen Sie Dateiberechtigungen und Eigentümerschaft');\n}\n```\n\n### Verbindungsfehler\n\n```dart\ntry {\n  await sftp.listDir(path);\n} on SftpConnectionException {\n  showError('Verbindung verloren');\n  await reconnect();\n}\n```\n\n### Speicherplatzfehler\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpNoSpaceException {\n  showError('Festplatte auf dem Remote-Server voll');\n}\n```\n\n## Leistungsoptimierungen\n\n### Verzeichnis-Caching\n\n```dart\nclass DirectoryCache {\n  final Map<String, CachedDirectory> _cache = {};\n  final Duration ttl = Duration(minutes: 5);\n\n  Future<List<SftpFile>> list(String path) async {\n    final cached = _cache[path];\n    if (cached != null && !cached.isExpired) {\n      return cached.files;\n    }\n\n    final files = await sftp.listDir(path);\n    _cache[path] = CachedDirectory(files);\n    return files;\n  }\n}\n```\n\n### Lazy Loading\n\nFür große Verzeichnisse (>1000 Einträge):\n\n```dart\nList<SftpFile> loadPage(String path, int page, int pageSize) {\n  final all = cache[path] ?? [];\n  final start = page * pageSize;\n  final end = start + pageSize;\n  return all.sublist(start, end.clamp(0, all.length));\n}\n```\n\n### Paginierung\n\n```dart\nclass PaginatedDirectory {\n  static const pageSize = 100;\n\n  Future<List<SftpFile>> getPage(int page) async {\n    final offset = page * pageSize;\n    return await sftp.listDir(\n      path,\n      offset: offset,\n      limit: pageSize,\n    );\n  }\n}\n```\n"
  },
  {
    "path": "docs/src/content/docs/de/principles/ssh.md",
    "content": "---\ntitle: SSH-Verbindung\ndescription: Wie SSH-Verbindungen aufgebaut und verwaltet werden\n---\n\nVerständnis der SSH-Verbindungen in Server Box.\n\n## Verbindungsablauf\n\n```text\nBenutzereingabe → Spi-Konfiguration → genClient() → SSH-Client → Sitzung\n```\n\n### Schritt 1: Konfiguration\n\nDas `Spi` (Server Parameter Info) Modell enthält:\n\n```dart\nclass Spi {\n  String id;         // eindeutige ID / unique identifier\n  String name;       // Servername\n  String ip;         // IP-Adresse\n  int port;          // SSH-Port (Standard 22)\n  String user;       // Benutzername\n  String? pwd;       // Passwort (verschlüsselt)\n  String? keyId;     // SSH-Schlüssel-ID\n  String? jumpId;    // Jump-Server-ID\n  String? alterUrl;  // Alternative URL\n}\n```\n\n### Schritt 2: Client-Generierung\n\n`genClient(spi)` erstellt den SSH-Client:\n\n```dart\nFuture<SSHClient> genClient(Spi spi) async {\n  // 1. Socket aufbauen\n  var socket = await connect(spi.ip, spi.port);\n\n  // 2. Alternative URL versuchen, falls fehlgeschlagen\n  if (socket == null && spi.alterUrl != null) {\n    socket = await connect(spi.alterUrl, spi.port);\n  }\n\n  if (socket == null) {\n    throw ConnectionException('Unable to connect');\n  }\n\n  // 3. Authentifizieren\n  final client = SSHClient(\n    socket: socket,\n    username: spi.user,\n    onPasswordRequest: () => spi.pwd,\n    onIdentityRequest: () => loadKey(spi.keyId),\n  );\n\n  // 4. Host-Key verifizieren\n  await verifyHostKey(client, spi);\n\n  return client;\n}\n```\n\n### Schritt 3: Jump-Server (falls konfiguriert)\n\nFür Jump-Server, rekursive Verbindung:\n\n```dart\nif (spi.jumpId != null) {\n  final jumpClient = await genClient(getJumpSpi(spi.jumpId));\n  final forwarded = await jumpClient.forwardLocal(\n    spi.ip,\n    spi.port,\n  );\n  // Über weitergeleiteten Socket verbinden\n}\n```\n\n## Authentifizierungsmethoden\n\n### Passwort-Authentifizierung\n\n```dart\nonPasswordRequest: () => spi.pwd\n```\n\n- Passwort verschlüsselt in Hive gespeichert\n- Bei Verbindung entschlüsselt\n- Zur Verifizierung an den Server gesendet\n\n### Private-Key-Authentifizierung\n\n```dart\nonIdentityRequest: () async {\n  final key = await KeyStore.get(spi.keyId);\n  return decyptPem(key.pem, key.password);\n}\n```\n\n**Schlüssel-Ladeprozess:**\n1. Verschlüsselten Schlüssel aus `KeyStore` abrufen\n2. Passwort entschlüsseln (Biometrie/Eingabeaufforderung)\n3. PEM-Format parsen\n4. Zeilenenden standardisieren (LF)\n5. Zur Authentifizierung zurückgeben\n\n### Tastatur-Interaktiv (Keyboard-Interactive)\n\n```dart\nonUserInfoRequest: (instructions) async {\n  // Challenge-Response handhaben\n  return responses;\n}\n```\n\nUnterstützt:\n- Passwort-Authentifizierung\n- OTP-Token\n- Zwei-Faktor-Authentifizierung (2FA)\n\n## Host-Key-Verifizierung\n\n### Warum Host-Keys verifizieren?\n\nVerhindert **Man-in-the-Middle (MITM)** Angriffe, indem sichergestellt wird, dass Sie sich mit demselben Server verbinden.\n\n### Speicherformat\n\n```text\n{spi.id}::{keyType}\n```\n\nBeispiel:\n```text\nmein-server::ssh-ed25519\nmein-server::ecdsa-sha2-nistp256\n```\n\n### Fingerabdruck-Formate\n\n**MD5 Hex:**\n```text\naa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99\n```\n\n**Base64:**\n```text\nSHA256:AbCdEf1234567890...=\n```\n\n### Verifizierungsablauf\n\n```dart\nFuture<void> verifyHostKey(SSHClient client, Spi spi) async {\n  final key = await client.hostKey;\n  final keyType = key.type;\n  final fingerprint = md5Hex(key); // oder base64\n\n  final stored = SettingStore.sshKnownHostsFingerprints\n      ['${spi.id}::$keyType'];\n\n  if (stored == null) {\n    // Neuer Host - Benutzer fragen\n    final trust = await promptUser(\n      'Unbekannter Host',\n      'Fingerabdruck: $fingerprint',\n    );\n    if (trust) {\n      SettingStore.sshKnownHostsFingerprints\n          ['${spi.id}::$keyType'] = fingerprint;\n    }\n  } else if (stored != fingerprint) {\n    // Geändert - Benutzer warnen\n    await warnUser(\n      'Host-Key geändert!',\n      'Möglicher MITM-Angriff',\n    );\n  }\n}\n```\n\n## Sitzungsverwaltung\n\n### Verbindungs-Pooling\n\nAktive Clients werden im `ServerProvider` verwaltet:\n\n```dart\nclass ServerProvider {\n  final Map<String, SSHClient> _clients = {};\n\n  SSHClient getClient(String spiId) {\n    return _clients[spiId] ??= connect(spiId);\n  }\n}\n```\n\n### Keep-Alive\n\nVerbindung bei Inaktivität aufrechterhalten:\n\n```dart\nTimer.periodic(\n  Duration(seconds: 30),\n  (_) => client.sendKeepAlive(),\n);\n```\n\n### Automatische Wiederverbindung\n\nBei Verbindungsverlust:\n\n```dart\nclient.onError.listen((error) async {\n  await Future.delayed(Duration(seconds: 5));\n  reconnect();\n});\n```\n\n## Lebenszyklus einer Verbindung\n\n```text\n┌─────────────┐\n│   Initial   │\n└──────┬──────┘\n       │ connect()\n       ↓\n┌─────────────┐\n│ Verbinden   │ ←──┐\n└──────┬──────┘   │\n       │ Erfolg   │\n       ↓          │ Fehler (Retry)\n┌─────────────┐   │\n│ Verbunden   │───┘\n└──────┬──────┘\n       │\n       ↓\n┌─────────────┐\n│   Aktiv     │ ──→ Befehle senden\n└──────┬──────┘\n       │\n       ↓ (Fehler/Trennung)\n┌─────────────┐\n│ Getrennt    │\n└─────────────┘\n```\n\n## Fehlerbehandlung\n\n### Verbindungs-Timeout\n\n```dart\ntry {\n  await client.connect().timeout(\n    Duration(seconds: 30),\n  );\n} on TimeoutException {\n  throw ConnectionException('Verbindungs-Timeout');\n}\n```\n\n### Authentifizierungsfehler\n\n```dart\nonAuthFail: (error) {\n  if (error.contains('password')) {\n    return 'Ungültiges Passwort';\n  } else if (error.contains('key')) {\n    return 'Ungültiger SSH-Schlüssel';\n  }\n  return 'Authentifizierung fehlgeschlagen';\n}\n```\n\n### Host-Key-Abweichung\n\n```dart\nonHostKeyMismatch: (stored, current) {\n  showSecurityWarning(\n    'Host-Key hat sich geändert!',\n    'Möglicher MITM-Angriff',\n  );\n}\n```\n\n## Leistungsaspekte\n\n### Wiederverwendung von Verbindungen\n\n- Wiederverwendung von Clients über Funktionen hinweg\n- Nicht unnötig trennen/wiederverbinden\n- Verbindungs-Pooling für gleichzeitige Operationen\n\n### Optimale Einstellungen\n\n- **Timeout**: 30 Sekunden (anpassbar)\n- **Keep-Alive**: Alle 30 Sekunden\n- **Wiederholungsverzögerung**: 5 Sekunden\n\n### Netzwerkeffizienz\n\n- Einzelne Verbindung für mehrere Operationen\n- Befehle pipelinen, wenn möglich\n- Das Öffnen mehrerer Verbindungen vermeiden\n"
  },
  {
    "path": "docs/src/content/docs/de/principles/state.md",
    "content": "---\ntitle: Zustandsverwaltung\ndescription: Wie der Zustand mit Riverpod verwaltet wird\n---\n\nVerständnis der Architektur zur Zustandsverwaltung in Server Box.\n\n## Warum Riverpod?\n\n**Hauptvorteile:**\n- **Sicherheit zur Kompilierzeit**: Fehler werden bereits beim Kompilieren abgefangen\n- **Kein BuildContext erforderlich**: Zugriff auf den Zustand von überall aus\n- **Einfache Testbarkeit**: Provider können leicht isoliert getestet werden\n- **Codegenerierung**: Weniger Boilerplate, typsicher\n\n## Provider-Architektur\n\n```\n┌─────────────────────────────────────────────┐\n│         UI-Schicht (Widgets)                │\n│  - ConsumerWidget / ConsumerStatefulWidget  │\n│  - ref.watch() / ref.read()                 │\n└─────────────────────────────────────────────┘\n                ↓ beobachtet (watches)\n┌─────────────────────────────────────────────┐\n│         Provider-Schicht                    │\n│  - @riverpod Annotationen                   │\n│  - Generierte *.g.dart Dateien              │\n└─────────────────────────────────────────────┘\n                ↓ nutzt (uses)\n┌─────────────────────────────────────────────┐\n│         Service- / Store-Schicht            │\n│  - Business-Logik                           │\n│  - Datenzugriff                             │\n└─────────────────────────────────────────────┘\n```\n\n## Verwendete Provider-Typen\n\n### 1. StateProvider (Einfacher Zustand)\n\nFür einfachen, beobachtbaren Zustand:\n\n```dart\n@riverpod\nclass ThemeNotifier extends _$ThemeNotifier {\n  @override\n  ThemeMode build() {\n    // Aus Einstellungen laden\n    return SettingStore.themeMode;\n  }\n\n  void setTheme(ThemeMode mode) {\n    state = mode;\n    SettingStore.themeMode = mode;  // Persistieren\n  }\n}\n```\n\n**Verwendung:**\n```dart\nclass MyWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final theme = ref.watch(themeNotifierProvider);\n    return Text('Theme: $theme');\n  }\n}\n```\n\n### 2. AsyncNotifierProvider (Asynchroner Zustand)\n\nFür Daten, die asynchron geladen werden:\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    // Initiales Laden\n    return await fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() async {\n      return await fetchStatus(server);\n    });\n  }\n}\n```\n\n**Verwendung:**\n```dart\nfinal status = ref.watch(serverStatusProvider(server));\n\nstatus.when(\n  data: (data) => StatusWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 3. StreamProvider (Echtzeit-Daten)\n\nFür kontinuierliche Datenströme:\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  final client = ref.watch(sshClientProvider(server));\n  final stream = client.monitorCpu();\n\n  // Ressourcen freigeben, wenn nicht mehr beobachtet\n  ref.onDispose(() {\n    client.stopMonitoring();\n  });\n\n  return stream;\n}\n```\n\n**Verwendung:**\n```dart\nfinal cpu = ref.watch(cpuUsageProvider(server));\n\ncpu.when(\n  data: (usage) => CpuChart(usage),\n  loading: () => CircularProgressIndicator(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 4. Family Provider (Parametrisiert)\n\nProvider, die Parameter akzeptieren:\n\n```dart\n@riverpod\nFuture<List<Container>> containers(ContainersRef ref, Server server) async {\n  final client = await ref.watch(sshClientProvider(server).future);\n  return await client.listContainers();\n}\n```\n\n**Verwendung:**\n```dart\nfinal containers = ref.watch(containersProvider(server));\n\n// Verschiedene Server = verschiedene gecachte Zustände\nfinal containers2 = ref.watch(containersProvider(server2));\n```\n\n## Muster für Zustandsaktualisierungen\n\n### Direkte Zustandsaktualisierung\n\n```dart\nref.read(settingsProvider.notifier).updateTheme(darkMode);\n```\n\n### Berechneter Zustand (Computed State)\n\n```dart\n@riverpod\nint totalServers(TotalServersRef ref) {\n  final servers = ref.watch(serversProvider);\n  return servers.length;\n}\n```\n\n### Abgeleiteter Zustand (Derived State)\n\n```dart\n@riverpod\nList<Server> onlineServers(OnlineServersRef ref) {\n  final all = ref.watch(serversProvider);\n  return all.where((s) => s.isOnline).toList();\n}\n```\n\n## Server-spezifischer Zustand\n\n### Pro-Server Provider\n\nJeder Server hat einen isolierten Zustand:\n\n```dart\n@riverpod\nclass ServerProvider extends _$ServerProvider {\n  @override\n  ServerState build(Server server) {\n    return ServerState.disconnected();\n  }\n\n  Future<void> connect() async {\n    state = ServerState.connecting();\n    try {\n      final client = await genClient(server.spi);\n      state = ServerState.connected(client);\n    } catch (e) {\n      state = ServerState.error(e.toString());\n    }\n  }\n}\n```\n\n## Leistungsoptimierungen\n\n- **Provider Keep-Alive**: `@Riverpod(keepAlive: true)` verwenden, um automatische Entsorgung ohne Listener zu verhindern.\n- **Selektives Beobachten**: `select` verwenden, um nur bestimmte Teile des Zustands zu beobachten.\n- **Provider Caching**: Family Provider cachen Ergebnisse pro Parameter.\n\n## Best Practices\n\n1. **Provider lokal platzieren**: In der Nähe der Widgets, die sie nutzen.\n2. **Codegenerierung nutzen**: Immer `@riverpod` verwenden.\n3. **Provider fokussiert halten**: Jedes Provider sollte nur eine Aufgabe haben.\n4. **Ladezustände behandeln**: AsyncValue-Zustände immer vollständig behandeln.\n5. **Ressourcen entsorgen**: `ref.onDispose()` für Aufräumarbeiten nutzen.\n6. **Tiefe Provider-Bäume vermeiden**: Den Provider-Graphen flach halten.\n"
  },
  {
    "path": "docs/src/content/docs/de/principles/terminal.md",
    "content": "---\ntitle: Terminal-Implementierung\ndescription: Wie das SSH-Terminal intern funktioniert\n---\n\nDas SSH-Terminal ist eine der komplexesten Funktionen, aufgebaut auf einem benutzerdefinierten xterm.dart-Fork.\n\n## Architektur-Übersicht\n\n```\n┌─────────────────────────────────────────────┐\n│          Terminal UI Schicht                │\n│  - Tab-Management                           │\n│  - Virtuelle Tastatur                       │\n│  - Textauswahl                              │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         xterm.dart Emulator                 │\n│  - PTY (Pseudo Terminal)                    │\n│  - VT100/ANSI Emulation                     │\n│  - Rendering-Engine                         │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          SSH-Client-Schicht                 │\n│  - SSH-Sitzung                              │\n│  - Kanalverwaltung                          │\n│  - Daten-Streaming                          │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          Remote-Server                      │\n│  - Shell-Prozess                            │\n│  - Befehlsausführung                        │\n└─────────────────────────────────────────────┘\n```\n\n## Lebenszyklus einer Terminal-Sitzung\n\n### 1. Sitzungserstellung\n\n```dart\nFuture<TerminalSession> createSession(Spi spi) async {\n  // 1. SSH-Client abrufen\n  final client = await genClient(spi);\n\n  // 2. PTY erstellen\n  final pty = await client.openPty(\n    term: 'xterm-256color',\n    cols: 80,\n    rows: 24,\n  );\n\n  // 3. Terminal-Emulator initialisieren\n  final terminal = Terminal(\n    backend: PtyBackend(pty),\n  );\n\n  // 4. Resize-Handler einrichten\n  terminal.onResize.listen((size) {\n    pty.resize(size.cols, size.rows);\n  });\n\n  return TerminalSession(\n    terminal: terminal,\n    pty: pty,\n    client: client,\n  );\n}\n```\n\n### 2. Terminal-Emulation\n\nDer xterm.dart-Fork bietet:\n\n**VT100/ANSI Emulation:**\n- Cursor-Bewegung\n- Farben (256-Farben-Unterstützung)\n- Textattribute (fett, unterstrichen, usw.)\n- Scroll-Bereiche\n- Alternativer Bildschirmpuffer\n\n**Rendering:**\n- Zeilenbasiertes Rendering\n- Unterstützung für bidirektionalen Text\n- Unicode/Emoji Unterstützung\n- Optimierte Redraws\n\n### 3. Datenfluss\n\n```\nBenutzereingabe\n    ↓\nVirtuelle Tastatur / Physische Tastatur\n    ↓\nTerminal-Emulator (Taste → Escape-Sequenz)\n    ↓\nSSH-Kanal (senden)\n    ↓\nRemote PTY\n    ↓\nRemote Shell\n    ↓\nBefehlsausgabe\n    ↓\nSSH-Kanal (empfangen)\n    ↓\nTerminal-Emulator (Analyse von ANSI-Codes)\n    ↓\nRendering auf dem Bildschirm\n```\n\n## Multi-Tab System\n\n### Tab-Management\n\nTabs behalten ihren Zustand bei Navigationswechseln bei:\n- SSH-Verbindung bleibt aktiv\n- Terminalzustand bleibt erhalten\n- Scroll-Puffer bleibt bestehen\n- Eingabeverlauf bleibt erhalten\n\n## Virtuelle Tastatur\n\n### Plattformspezifische Implementierung\n\n**iOS:**\n- UIView-basierte benutzerdefinierte Tastatur\n- Umschaltbar mit Tastatur-Button\n- Automatisches Ein-/Ausblenden basierend auf dem Fokus\n\n**Android:**\n- Benutzerdefinierte Eingabemethode\n- Integriert in die Systemtastatur\n- Schnellaktionstasten\n\n### Tastatur-Buttons\n\n| Button | Aktion |\n|--------|--------|\n| **Umschalten** | Systemtastatur ein-/ausblenden |\n| **Ctrl** | Ctrl-Modifikator senden |\n| **Alt** | Alt-Modifikator senden |\n| **SFTP** | Aktuelles Verzeichnis öffnen |\n| **Zwischenablage** | Kontextsensitive Kopieren/Einfügen |\n| **Snippets** | Snippet ausführen |\n\n## Textauswahl\n\n1. **Langes Drücken**: Auswahlmodus aktivieren\n2. **Ziehen**: Auswahl erweitern\n3. **Loslassen**: In die Zwischenablage kopieren\n\n## Schriftart und Dimensionen\n\n### Größenberechnung\n\n```dart\nclass TerminalDimensions {\n  static Size calculate(double fontSize, Size screenSize) {\n    final charWidth = fontSize * 0.6;  // Monospace-Seitenverhältnis\n    final charHeight = fontSize * 1.2;\n\n    final cols = (screenSize.width / charWidth).floor();\n    final rows = (screenSize.height / charHeight).floor();\n\n    return Size(cols.toDouble(), rows.toDouble());\n  }\n}\n```\n\n### Pinch-to-Zoom\n\n```dart\nGestureDetector(\n  onScaleStart: () => _baseFontSize = currentFontSize,\n  onScaleUpdate: (details) {\n    final newFontSize = _baseFontSize * details.scale;\n    resize(newFontSize);\n  },\n)\n```\n\n## Farbschema\n\n- **Hell (Light)**: Heller Hintergrund, dunkler Text\n- **Dunkel (Dark)**: Dunkler Hintergrund, heller Text\n- **AMOLED**: Rein schwarzer Hintergrund\n\n## Leistungsoptimierungen\n\n- **Dirty Rectangle**: Nur geänderte Regionen neu zeichnen\n- **Zeilen-Caching**: Gerenderte Zeilen cachen\n- **Lazy Scrolling**: Virtuelles Scrollen für lange Puffer\n- **Batch-Updates**: Mehrere Schreibvorgänge zusammenfassen\n- **Kompression**: Kompression des Scroll-Puffers\n- **Debouncing**: Debouncing für schnelle Eingaben\n"
  },
  {
    "path": "docs/src/content/docs/de/quick-start.mdx",
    "content": "---\ntitle: Schnellstart\ndescription: In wenigen Minuten mit Server Box loslegen\n---\n\nFolgen Sie dieser Schnellstartanleitung, um sich mit Ihrem ersten Server zu verbinden und mit der Überwachung zu beginnen.\n\n## Schritt 1: Einen Server hinzufügen\n\n1. Öffnen Sie Server Box\n2. Tippen Sie auf die Schaltfläche **+**, um einen neuen Server hinzuzufügen\n3. Geben Sie die Serverinformationen ein:\n   - **Name**: Ein Anzeigename für Ihren Server\n   - **Host**: IP-Adresse oder Domainname\n   - **Port**: SSH-Port (Standard: 22)\n   - **Benutzer**: SSH-Benutzername\n   - **Passwort oder Schlüssel**: Authentifizierungsmethode\n\n4. Tippen Sie auf **Speichern**, um den Server hinzuzufügen\n\n## Schritt 2: Verbinden und Überwachen\n\n1. Tippen Sie auf Ihre Serverkarte, um die Verbindung herzustellen\n2. Die App baut eine SSH-Verbindung auf\n3. Sie sehen den Echtzeit-Status für:\n   - CPU-Auslastung\n   - Arbeitsspeicher (RAM) und Swap\n   - Festplattenbelegung\n   - Netzwerkgeschwindigkeit\n\n## Schritt 3: Funktionen erkunden\n\nSobald die Verbindung hergestellt ist, können Sie:\n\n- **Terminal öffnen**: Tippen Sie auf die Terminal-Schaltfläche für vollen SSH-Zugriff\n- **Dateien durchsuchen**: Verwenden Sie SFTP, um Dateien zu verwalten\n- **Container verwalten**: Docker-Container anzeigen und steuern\n- **Prozesse anzeigen**: Laufende Prozesse überprüfen\n- **Snippets ausführen**: Gespeicherte Befehle ausführen\n\n## Tipps\n\n- **Biometrische Authentifizierung**: Aktivieren Sie Face ID / Touch ID / Fingerabdruck für schnellen Zugriff (Mobilgerät)\n- **Startbildschirm-Widgets**: Fügen Sie Serverstatus-Widgets zu Ihrem Startbildschirm hinzu (iOS/Android)\n- **Hintergrundbetrieb**: Halten Sie Verbindungen im Hintergrund aktiv (Android)\n"
  },
  {
    "path": "docs/src/content/docs/development/architecture.md",
    "content": "---\ntitle: Architecture\ndescription: Architecture patterns and design decisions\n---\n\nServer Box follows clean architecture principles with clear separation between data, domain, and presentation layers.\n\n## Layered Architecture\n\n```\n┌─────────────────────────────────────┐\n│          Presentation Layer         │\n│         (lib/view/page/)            │\n│  - Pages, Widgets, Controllers      │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│         Business Logic Layer        │\n│      (lib/data/provider/)           │\n│  - Riverpod Providers               │\n│  - State Management                 │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│           Data Layer                │\n│      (lib/data/model/, store/)      │\n│  - Models, Storage, Services        │\n└─────────────────────────────────────┘\n```\n\n## Key Patterns\n\n### State Management: Riverpod\n\n- **Code Generation**: Uses `riverpod_generator` for type-safe providers\n- **State Notifiers**: For mutable state with business logic\n- **Async Notifiers**: For loading and error states\n- **Stream Providers**: For real-time data\n\n### Immutable Models: Freezed\n\n- All data models use Freezed for immutability\n- Union types for state representation\n- Built-in JSON serialization\n- CopyWith extensions for updates\n\n### Local Storage: Hive\n\n- **hive_ce**: Community edition of Hive\n- No manual `@HiveField` or `@HiveType` needed\n- Type adapters auto-generated\n- Persistent key-value storage\n\n## Dependency Injection\n\nServices and stores are injected via:\n\n1. **Providers**: Expose dependencies to UI\n2. **GetIt**: Service location (where applicable)\n3. **Constructor Injection**: Explicit dependencies\n\n## Data Flow\n\n```\nUser Action → Widget → Provider → Service/Store → Model Update → UI Rebuild\n```\n\n1. User interacts with widget\n2. Widget calls provider method\n3. Provider updates state via service/store\n3. State change triggers UI rebuild\n4. New state reflected in widget\n\n## Custom Dependencies\n\nThe project uses several custom forks to extend functionality:\n\n- **dartssh2**: Enhanced SSH features\n- **xterm**: Terminal emulator with mobile support\n- **fl_lib**: Shared UI components and utilities\n\n## Threading\n\n- **Isolates**: Heavy computation off main thread\n- **computer package**: Multi-threading utilities\n- **Async/Await**: Non-blocking I/O operations\n"
  },
  {
    "path": "docs/src/content/docs/development/building.md",
    "content": "---\ntitle: Building\ndescription: Build instructions for different platforms\n---\n\nServer Box uses a custom build system (`fl_build`) for cross-platform builds.\n\n## Prerequisites\n\n- Flutter SDK (stable channel)\n- Platform-specific tools (Xcode for iOS, Android Studio for Android)\n- Rust toolchain (for some native dependencies)\n\n## Development Build\n\n```bash\n# Run in development mode\nflutter run\n\n# Run on specific device\nflutter run -d <device-id>\n```\n\n## Production Build\n\nThe project uses `fl_build` for building:\n\n```bash\n# Build for specific platform\ndart run fl_build -p <platform>\n\n# Available platforms:\n# - ios\n# - android\n# - macos\n# - linux\n# - windows\n```\n\n## Platform-Specific Builds\n\n### iOS\n\n```bash\ndart run fl_build -p ios\n```\n\nRequires:\n- macOS with Xcode\n- CocoaPods\n- Apple Developer account for signing\n\n### Android\n\n```bash\ndart run fl_build -p android\n```\n\nRequires:\n- Android SDK\n- Java Development Kit\n- Keystore for signing\n\n### macOS\n\n```bash\ndart run fl_build -p macos\n```\n\n### Linux\n\n```bash\ndart run fl_build -p linux\n```\n\n### Windows\n\n```bash\ndart run fl_build -p windows\n```\n\nRequires Windows with Visual Studio.\n\n## Pre/Post Build\n\nThe `make.dart` script handles:\n\n- Metadata generation\n- Version string updates\n- Platform-specific configurations\n\n## Troubleshooting\n\n### Clean Build\n\n```bash\nflutter clean\ndart run build_runner build --delete-conflicting-outputs\nflutter pub get\n```\n\n### Version Mismatch\n\nEnsure all dependencies are compatible:\n```bash\nflutter pub upgrade\n```\n\n## Release Checklist\n\n1. Update version in `pubspec.yaml`\n2. Run code generation\n3. Run tests\n4. Build for all target platforms\n5. Test on physical devices\n6. Create GitHub release\n"
  },
  {
    "path": "docs/src/content/docs/development/codegen.md",
    "content": "---\ntitle: Code Generation\ndescription: Using build_runner for code generation\n---\n\nServer Box heavily uses code generation for models, state management, and serialization.\n\n## When to Run Code Generation\n\nRun after modifying:\n\n- Models with `@freezed` annotation\n- Classes with `@JsonSerializable`\n- Hive models\n- Providers with `@riverpod`\n- Localizations (ARB files)\n\n## Running Code Generation\n\n```bash\n# Generate all code\ndart run build_runner build --delete-conflicting-outputs\n\n# Clean and regenerate\ndart run build_runner build --delete-conflicting-outputs --clean\n```\n\n## Generated Files\n\n### Freezed (`*.freezed.dart`)\n\nImmutable data models with union types:\n\n```dart\n@freezed\nclass ServerState with _$ServerState {\n  const factory ServerState.connected() = Connected;\n  const factory ServerState.disconnected() = Disconnected;\n  const factory ServerState.error(String message) = Error;\n}\n```\n\n### JSON Serialization (`*.g.dart`)\n\nGenerated from `json_serializable`:\n\n```dart\n@JsonSerializable()\nclass Server {\n  final String id;\n  final String name;\n  final String host;\n\n  Server({required this.id, required this.name, required this.host});\n\n  factory Server.fromJson(Map<String, dynamic> json) =>\n      _$ServerFromJson(json);\n  Map<String, dynamic> toJson() => _$ServerToJson(this);\n}\n```\n\n### Riverpod Providers (`*.g.dart`)\n\nGenerated from `@riverpod` annotation:\n\n```dart\n@riverpod\nclass MyNotifier extends _$MyNotifier {\n  @override\n  int build() => 0;\n}\n```\n\n### Hive Adapters (`*.g.dart`)\n\nAuto-generated for Hive models (hive_ce):\n\n```dart\n@HiveType(typeId: 0)\nclass ServerModel {\n  @HiveField(0)\n  final String id;\n}\n```\n\n## Localization Generation\n\n```bash\nflutter gen-l10n\n```\n\nGenerates `lib/generated/l10n/` from `lib/l10n/*.arb` files.\n\n## Tips\n\n- Use `--delete-conflicting-outputs` to avoid conflicts\n- Add generated files to `.gitignore`\n- Never manually edit generated files\n"
  },
  {
    "path": "docs/src/content/docs/development/state.md",
    "content": "---\ntitle: State Management\ndescription: Riverpod-based state management patterns\n---\n\nServer Box uses Riverpod with code generation for state management.\n\n## Provider Types\n\n### StateProvider\n\nSimple state that can be read and written:\n\n```dart\n@riverpod\nclass Settings extends _$Settings {\n  @override\n  SettingsModel build() {\n    return SettingsModel.defaults();\n  }\n\n  void update(SettingsModel newSettings) {\n    state = newSettings;\n  }\n}\n```\n\n### AsyncNotifierProvider\n\nState that loads asynchronously with loading/error states:\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    return fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() => fetchStatus(server));\n  }\n}\n```\n\n### StreamProvider\n\nReal-time data from streams:\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  return cpuService.monitor(server);\n}\n```\n\n## State Patterns\n\n### Loading States\n\n```dart\nstate.when(\n  data: (data) => DataWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### Family Providers\n\nParameterized providers:\n\n```dart\n@riverpod\nList<Container> containers(ContainersRef ref, Server server) {\n  return containerService.list(server);\n}\n```\n\n### Auto-Dispose\n\nProviders that dispose when no longer referenced:\n\n```dart\n@Riverpod(keepAlive: false)\nclass TempState extends _$TempState {\n  // ...\n}\n```\n\n## Best Practices\n\n1. **Use code generation**: Always use `@riverpod` annotation\n2. **Co-locate providers**: Place near consuming widgets\n3. **Avoid singletons**: Use providers instead\n4. **Layer correctly**: Keep UI logic separate from business logic\n\n## Reading State in Widgets\n\n```dart\nclass ServerWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final status = ref.watch(serverStatusProvider(server));\n    return status.when(...);\n  }\n}\n```\n\n## Modifying State\n\n```dart\nref.read(settingsProvider.notifier).update(newSettings);\n```\n"
  },
  {
    "path": "docs/src/content/docs/development/structure.md",
    "content": "---\ntitle: Project Structure\ndescription: Understanding the Server Box codebase\n---\n\nThe Server Box project follows a modular architecture with clear separation of concerns.\n\n## Directory Structure\n\n```\nlib/\n├── core/              # Core utilities and extensions\n├── data/              # Data layer\n│   ├── model/         # Data models by feature\n│   ├── provider/      # Riverpod providers\n│   └── store/         # Local storage (Hive)\n├── view/              # UI layer\n│   ├── page/          # Main pages\n│   └── widget/        # Reusable widgets\n├── generated/         # Generated localization\n├── l10n/              # Localization ARB files\n└── hive/              # Hive adapters\n```\n\n## Core Layer (`lib/core/`)\n\nContains utilities, extensions, and routing configuration:\n\n- **Extensions**: Dart extensions for common types\n- **Routes**: App routing configuration\n- **Utils**: Shared utility functions\n\n## Data Layer (`lib/data/`)\n\n### Models (`lib/data/model/`)\n\nOrganized by feature:\n\n- `server/` - Server connection and status models\n- `container/` - Docker container models\n- `ssh/` - SSH session models\n- `sftp/` - SFTP file models\n- `app/` - App-specific models\n\n### Providers (`lib/data/provider/`)\n\nRiverpod providers for dependency injection and state management:\n\n- Server providers\n- UI state providers\n- Service providers\n\n### Stores (`lib/data/store/`)\n\nHive-based local storage:\n\n- Server storage\n- Settings storage\n- Cache storage\n\n## View Layer (`lib/view/`)\n\n### Pages (`lib/view/page/`)\n\nMain application screens:\n\n- `server/` - Server management pages\n- `ssh/` - SSH terminal pages\n- `container/` - Container pages\n- `setting/` - Settings pages\n- `storage/` - SFTP pages\n- `snippet/` - Snippet pages\n\n### Widgets (`lib/view/widget/`)\n\nReusable UI components:\n\n- Server cards\n- Status charts\n- Input components\n- Dialogs\n\n## Generated Files\n\n- `lib/generated/l10n/` - Auto-generated localization\n- `*.g.dart` - Generated code (json_serializable, freezed, hive, riverpod)\n- `*.freezed.dart` - Freezed immutable classes\n\n## Packages Directory (`/packages/`)\n\nContains custom forks of dependencies:\n\n- `dartssh2/` - SSH library\n- `xterm/` - Terminal emulator\n- `fl_lib/` - Shared utilities\n- `fl_build/` - Build system\n"
  },
  {
    "path": "docs/src/content/docs/development/testing.md",
    "content": "---\ntitle: Testing\ndescription: Testing strategies and running tests\n---\n\n## Running Tests\n\n```bash\n# Run all tests\nflutter test\n\n# Run specific test file\nflutter test test/battery_test.dart\n\n# Run with coverage\nflutter test --coverage\n```\n\n## Test Structure\n\nTests are located in the `test/` directory mirroring the lib structure:\n\n```\ntest/\n├── data/\n│   ├── model/\n│   └── provider/\n├── view/\n│   └── widget/\n└── test_helpers.dart\n```\n\n## Unit Tests\n\nTest business logic and data models:\n\n```dart\ntest('should calculate CPU percentage', () {\n  final cpu = CpuModel(usage: 75.0);\n  expect(cpu.usagePercentage, '75%');\n});\n```\n\n## Widget Tests\n\nTest UI components:\n\n```dart\ntestWidgets('ServerCard displays server name', (tester) async {\n  await tester.pumpWidget(\n    ProviderScope(\n      child: MaterialApp(\n        home: ServerCard(server: testServer),\n      ),\n    ),\n  );\n\n  expect(find.text('Test Server'), findsOneWidget);\n});\n```\n\n## Provider Tests\n\nTest Riverpod providers:\n\n```dart\ntest('serverStatusProvider returns status', () async {\n  final container = ProviderContainer();\n  final status = await container.read(serverStatusProvider(testServer).future);\n  expect(status, isA<StatusModel>());\n});\n```\n\n## Mocking\n\nUse mocks for external dependencies:\n\n```dart\nclass MockSshService extends Mock implements SshService {}\n\ntest('connects to server', () async {\n  final mockSsh = MockSshService();\n  when(mockSsh.connect(any)).thenAnswer((_) async => true);\n\n  // Test with mock\n});\n```\n\n## Integration Tests\n\nTest complete user flows (in `integration_test/`):\n\n```dart\ntestWidgets('add server flow', (tester) async {\n  await tester.pumpWidget(MyApp());\n\n  // Tap add button\n  await tester.tap(find.byIcon(Icons.add));\n  await tester.pumpAndSettle();\n\n  // Fill form\n  await tester.enterText(find.byKey(Key('name')), 'Test Server');\n  // ...\n});\n```\n\n## Best Practices\n\n1. **Arrange-Act-Assert**: Structure tests clearly\n2. **Descriptive names**: Test names should describe behavior\n3. **One assertion per test**: Keep tests focused\n4. **Mock external deps**: Don't depend on real servers\n5. **Test edge cases**: Empty lists, null values, etc.\n"
  },
  {
    "path": "docs/src/content/docs/es/advanced/bulk-import.md",
    "content": "---\ntitle: Importación Masiva de Servidores\ndescription: Importar múltiples servidores desde un archivo JSON\n---\n\nImporta múltiples configuraciones de servidor a la vez utilizando un archivo JSON.\n\n## Formato JSON\n\n:::danger[Advertencia de Seguridad]\n**¡Nunca guardes contraseñas en texto plano en archivos!** Este ejemplo JSON muestra un campo de contraseña solo con fines demostrativos, pero deberías:\n\n- **Preferir claves SSH** (`keyId`) en lugar de `pwd`; son más seguras\n- **Usar gestores de secretos** o variables de entorno si debes usar contraseñas\n- **Eliminar el archivo inmediatamente** después de la importación; no dejes credenciales tiradas\n- **Añadir a .gitignore**: nunca subas archivos de credenciales al control de versiones\n:::\n\n```json\n[\n  {\n    \"name\": \"Mi Servidor\",\n    \"ip\": \"example.com\",\n    \"port\": 22,\n    \"user\": \"root\",\n    \"pwd\": \"password\",\n    \"keyId\": \"\",\n    \"tags\": [\"production\"],\n    \"autoConnect\": false\n  }\n]\n```\n\n## Campos\n\n| Campo | Requerido | Descripción |\n|-------|-----------|-------------|\n| `name` | Sí | Nombre para mostrar |\n| `ip` | Sí | Dominio o dirección IP |\n| `port` | Sí | Puerto SSH (usualmente 22) |\n| `user` | Sí | Usuario SSH |\n| `pwd` | No | Contraseña (evitar - usar claves SSH en su lugar) |\n| `keyId` | No | Nombre de la clave SSH (de Claves Privadas - recomendado) |\n| `tags` | No | Etiquetas de organización |\n| `autoConnect` | No | Autoconexión al iniciar |\n\n## Pasos para Importar\n\n1. Crea un archivo JSON con las configuraciones del servidor\n2. Ajustes → Copia de seguridad → Importación masiva de servidores\n3. Selecciona tu archivo JSON\n4. Confirma la importación\n\n## Ejemplo\n\n```json\n[\n  {\n    \"name\": \"Producción\",\n    \"ip\": \"prod.example.com\",\n    \"port\": 22,\n    \"user\": \"admin\",\n    \"keyId\": \"mi-clave\",\n    \"tags\": [\"production\", \"web\"]\n  },\n  {\n    \"name\": \"Desarrollo\",\n    \"ip\": \"dev.example.com\",\n    \"port\": 2222,\n    \"user\": \"dev\",\n    \"keyId\": \"dev-clave\",\n    \"tags\": [\"development\"]\n  }\n]\n```\n\n## Consejos\n\n- **Usa claves SSH** en lugar de contraseñas cuando sea posible\n- **Prueba la conexión** después de la importación\n- **Organiza con etiquetas** para una gestión más sencilla\n- **Elimina el archivo JSON** después de la importación\n- **Nunca subas** archivos JSON con credenciales al control de versiones\n"
  },
  {
    "path": "docs/src/content/docs/es/advanced/custom-commands.md",
    "content": "---\ntitle: Comandos Personalizados\ndescription: Mostrar la salida de comandos personalizados en la página del servidor\n---\n\nAñade comandos shell personalizados para mostrar su salida en la página de detalles del servidor.\n\n## Configuración\n\n1. Ajustes del servidor → Comandos personalizados\n2. Introduce los comandos en formato JSON\n\n## Formato Básico\n\n```json\n{\n  \"Nombre a mostrar\": \"comando shell\"\n}\n```\n\n**Ejemplo:**\n```json\n{\n  \"Memoria\": \"free -h\",\n  \"Disco\": \"df -h\",\n  \"Tiempo de actividad\": \"uptime\"\n}\n```\n\n## Ver Resultados\n\nTras la configuración, los comandos personalizados aparecerán en la página de detalles del servidor y se actualizarán automáticamente.\n\n## Nombres de Comando Especiales\n\n### server_card_top_right\n\nSe muestra en la tarjeta del servidor de la página de inicio (esquina superior derecha):\n\n```json\n{\n  \"server_card_top_right\": \"tu-comando-aquí\"\n}\n```\n\n## Consejos\n\n**Usa rutas absolutas:**\n```json\n{\"Mi Script\": \"/usr/local/bin/mi-script.sh\"}\n```\n\n**Comandos con tuberías (pipes):**\n```json\n{\"Proceso principal\": \"ps aux | sort -rk 3 | head -5\"}\n```\n\n**Formatear salida:**\n```json\n{\"Carga de CPU\": \"uptime | awk -F'load average:' '{print $2}'\"}\n```\n\n**Mantén los comandos rápidos:** Menos de 5 segundos para una mejor experiencia.\n\n**Limitar salida:**\n```json\n{\"Logs\": \"tail -20 /var/log/syslog\"}\n```\n\n## Seguridad\n\nLos comandos se ejecutan con los permisos del usuario SSH. Evita comandos que modifiquen el estado del sistema.\n"
  },
  {
    "path": "docs/src/content/docs/es/advanced/custom-logo.md",
    "content": "---\ntitle: Logo de Servidor Personalizado\ndescription: Usa imágenes personalizadas para las tarjetas de servidor\n---\n\nMuestra logos personalizados en las tarjetas de servidor mediante URLs de imagen.\n\n## Configuración\n\n1. Ajustes del servidor → Logo personalizado\n2. Introduce la URL de la imagen\n\n## Marcadores de posición de URL\n\n### {DIST} - Distribución Linux\n\nSe reemplaza automáticamente por la distribución detectada:\n\n```\nhttps://ejemplo.com/{DIST}.png\n```\n\nSe convierte en: `debian.png`, `ubuntu.png`, `arch.png`, etc.\n\n### {BRIGHT} - Tema\n\nSe reemplaza automáticamente por el tema actual:\n\n```\nhttps://ejemplo.com/{BRIGHT}.png\n```\n\nSe convierte en: `light.png` o `dark.png`\n\n### Combinar ambos\n\n```\nhttps://ejemplo.com/{DIST}-{BRIGHT}.png\n```\n\nSe convierte en: `debian-light.png`, `ubuntu-dark.png`, etc.\n\n## Consejos\n\n- Usa formatos PNG o SVG\n- Tamaño recomendado: de 64x64 a 128x128 píxeles\n- Usa URLs HTTPS\n- Mantén tamaños de archivo pequeños\n\n## Distribuciones Soportadas\n\ndebian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt\n\nLista completa: [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)\n"
  },
  {
    "path": "docs/src/content/docs/es/advanced/json-settings.md",
    "content": "---\ntitle: Ajustes Ocultos (JSON)\ndescription: Accede a ajustes avanzados mediante el editor JSON\n---\n\nAlgunos ajustes están ocultos en la interfaz de usuario pero son accesibles a través del editor JSON.\n\n## Acceso\n\nMantén pulsado **Ajustes** en el menú lateral para abrir el editor JSON.\n\n## Ajustes Ocultos Comunes\n\n### timeOut\n\nTiempo de espera de conexión en segundos.\n\n```json\n{\"timeOut\": 10}\n```\n\n**Tipo:** entero | **Predeterminado:** 5 | **Rango:** 1-60\n\n### recordHistory\n\nGuardar historial (rutas SFTP, etc.).\n\n```json\n{\"recordHistory\": true}\n```\n\n**Tipo:** booleano | **Predeterminado:** true\n\n### textFactor\n\nFactor de escala de texto.\n\n```json\n{\"textFactor\": 1.2}\n```\n\n**Tipo:** doble | **Predeterminado:** 1.0 | **Rango:** 0.8-1.5\n\n## Encontrar Más Ajustes\n\nTodos los ajustes están definidos en [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart).\n\nBusca:\n```dart\nlate final settingName = StoreProperty(box, 'settingKey', defaultValue);\n```\n\n## ⚠️ Importante\n\n**Antes de editar:**\n- **Crea una copia de seguridad**: unos ajustes incorrectos pueden hacer que la app no se abra\n- **Edita con cuidado**: el JSON debe ser válido\n\n## Recuperación\n\nSi la aplicación no se abre tras editar:\n1. Borra los datos de la aplicación (último recurso)\n2. Reinstala la aplicación\n3. Restaura desde una copia de seguridad\n"
  },
  {
    "path": "docs/src/content/docs/es/advanced/troubleshooting.md",
    "content": "---\ntitle: Problemas Comunes\ndescription: Soluciones a problemas frecuentes\n---\n\n## Problemas de Conexión\n\n### SSH no conecta\n\n**Síntomas:** Tiempo de espera agotado (timeout), conexión rechazada, fallo de autenticación\n\n**Soluciones:**\n\n1. **Verificar el tipo de servidor:** Solo se admiten sistemas tipo Unix (Linux, macOS, Android/Termux)\n2. **Probar manualmente:** `ssh usuario@servidor -p puerto`\n3. **Comprobar el cortafuegos:** El puerto 22 debe estar abierto\n4. **Verificar credenciales:** Usuario y contraseña/clave correctos\n\n### Desconexiones frecuentes\n\n**Síntomas:** El terminal se desconecta tras un periodo de inactividad\n\n**Soluciones:**\n\n1. **Keep-alive del servidor:**\n   ```bash\n   # /etc/ssh/sshd_config\n   ClientAliveInterval 60\n   ClientAliveCountMax 3\n   ```\n\n2. **Desactivar optimización de batería:**\n   - MIUI: Batería → \"Sin restricciones\"\n   - Android: Ajustes → Aplicaciones → Desactivar optimización\n   - iOS: Activar actualización en segundo plano\n\n## Problemas de Entrada\n\n### No se pueden escribir ciertos caracteres\n\n**Solución:** Ajustes → Tipo de teclado → Cambiar a `visiblePassword`\n\nNota: Es posible que la entrada CJK (chino, japonés, coreano) no funcione tras este cambio.\n\n## Problemas de la Aplicación\n\n### La aplicación se cierra al iniciar\n\n**Síntomas:** La aplicación no se abre, pantalla en negro\n\n**Causas:** Ajustes corruptos, especialmente tras usar el editor JSON\n\n**Soluciones:**\n\n1. **Borrar datos de la aplicación:**\n   - Android: Ajustes → Aplicaciones → ServerBox → Borrar datos\n   - iOS: Eliminar y reinstalar\n\n2. **Restaurar copia de seguridad:** Importar una copia de seguridad creada antes de cambiar los ajustes\n\n### Problemas con Copia de Seguridad/Restauración\n\n**La copia de seguridad no funciona:**\n- Comprobar espacio de almacenamiento\n- Verificar que la aplicación tiene permisos de almacenamiento\n- Probar una ubicación diferente\n\n**La restauración falla:**\n- Verificar la integridad del archivo de copia de seguridad\n- Comprobar la compatibilidad de la versión de la aplicación\n\n## Problemas con Widgets\n\n### El Widget no se actualiza\n\n**iOS:**\n- Esperar hasta 30 minutos para la actualización automática\n- Eliminar y volver a añadir el widget\n- Comprobar que la URL termina en `/status`\n\n**Android:**\n- Pulsar el widget para forzar la actualización\n- Verificar que el ID del widget coincide con la configuración en los ajustes de la aplicación\n\n**watchOS:**\n- Reiniciar la aplicación del reloj\n- Esperar unos minutos tras cambiar la configuración\n- Verificar el formato de la URL\n\n### El Widget muestra un error\n\n- Verificar que ServerBox Monitor se está ejecutando en el servidor\n- Probar la URL en un navegador\n- Comprobar las credenciales de autenticación\n\n## Problemas de Rendimiento\n\n### La aplicación va lenta\n\n**Soluciones:**\n- Reducir la tasa de refresco en los ajustes\n- Comprobar la velocidad de la red\n- Desactivar servidores no utilizados\n\n### Alto consumo de batería\n\n**Soluciones:**\n- Aumentar los intervalos de refresco\n- Desactivar la actualización en segundo plano\n- Cerrar sesiones SSH no utilizadas\n\n## Obtener Ayuda\n\nSi los problemas persisten:\n\n1. **Buscar en GitHub Issues:** https://github.com/lollipopkit/flutter_server_box/issues\n2. **Crear nueva Issue:** Incluir versión de la aplicación, plataforma y pasos para reproducir\n3. **Consultar la Wiki:** Esta documentación y la Wiki de GitHub\n"
  },
  {
    "path": "docs/src/content/docs/es/advanced/widgets.md",
    "content": "---\ntitle: Widgets de Pantalla de Inicio\ndescription: Añade widgets de estado del servidor a tu pantalla de inicio\n---\n\nRequiere tener instalado [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) en tus servidores.\n\n## Requisitos Previos\n\nInstala primero ServerBox Monitor en tu servidor. Consulta la [Wiki de ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor/wiki/Home) para ver las instrucciones de configuración.\n\nTras la instalación, tu servidor debería tener:\n- Un punto de acceso (endpoint) HTTP/HTTPS\n- El punto de acceso API `/status`\n- Autenticación opcional\n\n## Formato de URL\n\n```\nhttps://tu-servidor.com/status\n```\n\nDebe terminar en `/status`.\n\n## Widget de iOS\n\n### Configuración\n\n1. Mantén pulsada la pantalla de inicio → Toca el símbolo **+**\n2. Busca \"ServerBox\"\n3. Elige el tamaño del widget\n4. Mantén pulsado el widget → **Editar widget**\n5. Introduce la URL terminada en `/status`\n\n### Notas\n\n- Debe usar HTTPS (excepto IPs locales)\n- Tasa máxima de refresco: 30 minutos (límite de iOS)\n- Añade varios widgets para varios servidores\n\n## Widget de Android\n\n### Configuración\n\n1. Mantén pulsada la pantalla de inicio → **Widgets**\n2. Busca \"ServerBox\" → Añadir a la pantalla de inicio\n3. Anota el número de ID del widget que aparece\n4. Abre la app ServerBox → Ajustes\n5. Toca en **Configurar enlace de widget de inicio**\n6. Añade la entrada: `Widget ID` = `URL de estado`\n\nEjemplo:\n- Clave (Key): `17`\n- Valor (Value): `https://mi-servidor.com/status`\n\n7. Toca el widget en la pantalla de inicio para refrescarlo\n\n## Widget de watchOS\n\n### Configuración\n\n1. Abre la app en el iPhone → Ajustes\n2. **Ajustes de iOS** → **App del Watch**\n3. Toca en **Añadir URL**\n4. Introduce la URL terminada en `/status`\n5. Espera a que la app del reloj se sincronice\n\n### Notas\n\n- Prueba a reiniciar la app del reloj si no se actualiza\n- Verifica que el teléfono y el reloj están conectados\n\n## Solución de Problemas\n\n### El Widget no se actualiza\n\n**iOS:** Espera hasta 30 minutos, luego elimínalo y vuelve a añadirlo.\n**Android:** Toca el widget para forzar el refresco, verifica el ID en los ajustes.\n**watchOS:** Reinicia la app del reloj, espera unos minutos.\n\n### El Widget muestra un error\n\n- Verifica que ServerBox Monitor se está ejecutando\n- Prueba la URL en un navegador\n- Comprueba que la URL termina en `/status`\n\n## Seguridad\n\n- **Usa siempre HTTPS** cuando sea posible\n- **IPs locales solo** en redes de confianza\n"
  },
  {
    "path": "docs/src/content/docs/es/development/architecture.md",
    "content": "---\ntitle: Arquitectura\ndescription: Patrones de arquitectura y decisiones de diseño\n---\n\nServer Box sigue los principios de Clean Architecture con una clara separación entre las capas de datos, dominio y presentación.\n\n## Arquitectura por Capas\n\n```\n┌─────────────────────────────────────┐\n│          Capa de Presentación       │\n│         (lib/view/page/)            │\n│  - Páginas, Widgets, Controladores  │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│      Capa de Lógica de Negocio      │\n│      (lib/data/provider/)           │\n│  - Riverpod Providers               │\n│  - Gestión de Estado                │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│           Capa de Datos             │\n│      (lib/data/model/, store/)      │\n│  - Modelos, Almacén, Servicios      │\n└─────────────────────────────────────┘\n```\n\n## Patrones Clave\n\n### Gestión de Estado: Riverpod\n\n- **Generación de Código**: Usa `riverpod_generator` para providers con tipado seguro\n- **State Notifiers**: Para estados mutables con lógica de negocio\n- **Async Notifiers**: Para estados de carga y error\n- **Stream Providers**: Para datos en tiempo real\n\n### Modelos Inmutables: Freezed\n\n- Todos los modelos de datos usan Freezed para inmutabilidad\n- Tipos Union para representación de estados\n- Serialización JSON integrada\n- Extensiones CopyWith para actualizaciones\n\n### Almacenamiento Local: Hive\n\n- **hive_ce**: Edición comunitaria de Hive\n- No se requiere `@HiveField` o `@HiveType` manual\n- Adaptadores de tipo generados automáticamente\n- Almacenamiento persistente clave-valor\n\n## Inyección de Dependencias\n\nLos servicios y almacenes se inyectan a través de:\n\n1. **Providers**: Exponen dependencias a la UI\n2. **GetIt**: Localizador de servicios (donde sea aplicable)\n3. **Inyección en Constructor**: Dependencias explícitas\n\n## Flujo de Datos\n\n```\nAcción de Usuario → Widget → Provider → Servicio/Almacén → Actualización de Modelo → Reconstrucción de UI\n```\n\n1. El usuario interactúa con el widget\n2. El widget llama al método del provider\n3. El provider actualiza el estado a través del servicio/almacén\n4. El cambio de estado activa la reconstrucción de la UI\n5. El nuevo estado se refleja en el widget\n\n## Dependencias Personalizadas\n\nEl proyecto utiliza varias ramas (forks) personalizadas para extender la funcionalidad:\n\n- **dartssh2**: Funciones SSH mejoradas\n- **xterm**: Emulador de terminal con soporte móvil\n- **fl_lib**: Componentes de UI y utilidades compartidas\n\n## Multihilo\n\n- **Isolates**: Computación pesada fuera del hilo principal\n- **paquete computer**: Utilidades para multihilo\n- **Async/Await**: Operaciones de E/S no bloqueantes\n"
  },
  {
    "path": "docs/src/content/docs/es/development/building.md",
    "content": "---\ntitle: Compilación\ndescription: Instrucciones de compilación para diferentes plataformas\n---\n\nServer Box utiliza un sistema de compilación personalizado (`fl_build`) para compilaciones multiplataforma.\n\n## Requisitos Previos\n\n- Flutter SDK (canal stable)\n- Herramientas específicas de cada plataforma (Xcode para iOS, Android Studio para Android)\n- Cadena de herramientas de Rust (para algunas dependencias nativas)\n\n## Compilación de Desarrollo\n\n```bash\n# Ejecutar en modo desarrollo\nflutter run\n\n# Ejecutar en un dispositivo específico\nflutter run -d <id-del-dispositivo>\n```\n\n## Compilación de Producción\n\nEl proyecto utiliza `fl_build` para compilar:\n\n```bash\n# Compilar para una plataforma específica\ndart run fl_build -p <plataforma>\n\n# Plataformas disponibles:\n# - ios\n# - android\n# - macos\n# - linux\n# - windows\n```\n\n## Compilaciones Específicas por Plataforma\n\n### iOS\n\n```bash\ndart run fl_build -p ios\n```\n\nRequiere:\n- macOS con Xcode\n- CocoaPods\n- Cuenta de Apple Developer para la firma\n\n### Android\n\n```bash\ndart run fl_build -p android\n```\n\nRequiere:\n- Android SDK\n- Java Development Kit\n- Keystore para la firma\n\n### macOS\n\n```bash\ndart run fl_build -p macos\n```\n\n### Linux\n\n```bash\ndart run fl_build -p linux\n```\n\n### Windows\n\n```bash\ndart run fl_build -p windows\n```\n\nRequiere Windows con Visual Studio.\n\n## Pre/Post Compilación\n\nEl script `make.dart` se encarga de:\n\n- Generación de metadatos\n- Actualización de cadenas de versión\n- Configuraciones específicas de plataforma\n\n## Solución de Problemas\n\n### Compilación Limpia\n\n```bash\nflutter clean\ndart run build_runner build --delete-conflicting-outputs\nflutter pub get\n```\n\n### Discrepancia de Versión\n\nAsegúrate de que todas las dependencias son compatibles:\n```bash\nflutter pub upgrade\n```\n\n## Lista de Verificación de Lanzamiento\n\n1. Actualizar la versión en `pubspec.yaml`\n2. Ejecutar la generación de código\n3. Ejecutar las pruebas\n4. Compilar para todas las plataformas de destino\n5. Probar en dispositivos físicos\n6. Crear lanzamiento (release) en GitHub\n"
  },
  {
    "path": "docs/src/content/docs/es/development/codegen.md",
    "content": "---\ntitle: Generación de Código\ndescription: Uso de build_runner para la generación de código\n---\n\nServer Box utiliza intensivamente la generación de código para modelos, gestión de estado y serialización.\n\n## Cuándo Ejecutar la Generación de Código\n\nEjecutar tras modificar:\n\n- Modelos con la anotación `@freezed`\n- Clases con `@JsonSerializable`\n- Modelos de Hive\n- Providers con `@riverpod`\n- Localizaciones (archivos ARB)\n\n## Ejecutar la Generación de Código\n\n```bash\n# Generar todo el código\ndart run build_runner build --delete-conflicting-outputs\n\n# Limpiar y regenerar\ndart run build_runner build --delete-conflicting-outputs --clean\n```\n\n## Archivos Generados\n\n### Freezed (`*.freezed.dart`)\n\nModelos de datos inmutables con tipos Union:\n\n```dart\n@freezed\nclass ServerState with _$ServerState {\n  const factory ServerState.connected() = Connected;\n  const factory ServerState.disconnected() = Disconnected;\n  const factory ServerState.error(String message) = Error;\n}\n```\n\n### Serialización JSON (`*.g.dart`)\n\nGenerado por `json_serializable`:\n\n```dart\n@JsonSerializable()\nclass Server {\n  final String id;\n  final String name;\n  final String host;\n\n  Server({required this.id, required this.name, required this.host});\n\n  factory Server.fromJson(Map<String, dynamic> json) =>\n      _$ServerFromJson(json);\n  Map<String, dynamic> toJson() => _$ServerToJson(this);\n}\n```\n\n### Providers de Riverpod (`*.g.dart`)\n\nGenerados a partir de la anotación `@riverpod`:\n\n```dart\n@riverpod\nclass MyNotifier extends _$MyNotifier {\n  @override\n  int build() => 0;\n}\n```\n\n### Adaptadores de Hive (`*.g.dart`)\n\nAuto-generados para modelos de Hive (hive_ce):\n\n```dart\n@HiveType(typeId: 0)\nclass ServerModel {\n  @HiveField(0)\n  final String id;\n}\n```\n\n## Generación de Localización\n\n```bash\nflutter gen-l10n\n```\n\nGenera `lib/generated/l10n/` a partir de los archivos `lib/l10n/*.arb`.\n\n## Consejos\n\n- Usa `--delete-conflicting-outputs` para evitar conflictos\n- Añade los archivos generados al `.gitignore`\n- Nunca edites manualmente los archivos generados\n"
  },
  {
    "path": "docs/src/content/docs/es/development/state.md",
    "content": "---\ntitle: Gestión de Estado\ndescription: Patrones de gestión de estado basados en Riverpod\n---\n\nServer Box utiliza Riverpod con generación de código para la gestión de estado.\n\n## Tipos de Provider\n\n### StateProvider\n\nEstado simple que se puede leer y escribir:\n\n```dart\n@riverpod\nclass Settings extends _$Settings {\n  @override\n  SettingsModel build() {\n    return SettingsModel.defaults();\n  }\n\n  void update(SettingsModel newSettings) {\n    state = newSettings;\n  }\n}\n```\n\n### AsyncNotifierProvider\n\nEstado que se carga de forma asíncrona con estados de carga/error:\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    return fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() => fetchStatus(server));\n  }\n}\n```\n\n### StreamProvider\n\nDatos en tiempo real desde flujos (streams):\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  return cpuService.monitor(server);\n}\n```\n\n## Patrones de Estado\n\n### Estados de Carga\n\n```dart\nstate.when(\n  data: (data) => DataWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### Family Providers\n\nProviders parametrizados:\n\n```dart\n@riverpod\nList<Container> containers(ContainersRef ref, Server server) {\n  return containerService.list(server);\n}\n```\n\n### Auto-Dispose\n\nProviders que se eliminan cuando ya no están referenciados:\n\n```dart\n@Riverpod(keepAlive: false)\nclass TempState extends _$TempState {\n  // ...\n}\n```\n\n## Mejores Prácticas\n\n1. **Usar generación de código**: Usa siempre la anotación `@riverpod`\n2. **Co-localizar providers**: Ponlos cerca de los widgets que los consumen\n3. **Evitar singletons**: Usa providers en su lugar\n4. **Capas correctas**: Mantén la lógica de UI separada de la lógica de negocio\n\n## Leer el Estado en Widgets\n\n```dart\nclass ServerWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final status = ref.watch(serverStatusProvider(server));\n    return status.when(...);\n  }\n}\n```\n\n## Modificar el Estado\n\n```dart\nref.read(settingsProvider.notifier).update(newSettings);\n```\n"
  },
  {
    "path": "docs/src/content/docs/es/development/structure.md",
    "content": "---\ntitle: Estructura del Proyecto\ndescription: Comprendiendo la base de código de Server Box\n---\n\nEl proyecto Server Box sigue una arquitectura modular con una clara separación de responsabilidades.\n\n## Estructura de Directorios\n\n```\nlib/\n├── core/              # Utilidades centrales y extensiones\n├── data/              # Capa de datos\n│   ├── model/         # Modelos de datos por función\n│   ├── provider/      # Riverpod providers\n│   └── store/         # Almacenamiento local (Hive)\n├── view/              # Capa de UI\n│   ├── page/          # Páginas principales\n│   └── widget/        # Widgets reutilizables\n├── generated/         # Localización generada\n├── l10n/              # Archivos ARB de localización\n└── hive/              # Adaptadores de Hive\n```\n\n## Capa Central (`lib/core/`)\n\nContiene utilidades, extensiones y configuración de rutas:\n\n- **Extensions**: Extensiones de Dart para tipos comunes\n- **Routes**: Configuración de rutas de la app\n- **Utils**: Funciones de utilidad compartidas\n\n## Capa de Datos (`lib/data/`)\n\n### Modelos (`lib/data/model/`)\n\nOrganizados por función:\n\n- `server/` - Modelos de conexión y estado del servidor\n- `container/` - Modelos de contenedores Docker\n- `ssh/` - Modelos de sesión SSH\n- `sftp/` - Modelos de archivos SFTP\n- `app/` - Modelos específicos de la app\n\n### Providers (`lib/data/provider/`)\n\nProviders de Riverpod para inyección de dependencias y gestión de estado:\n\n- Providers de servidor\n- Providers de estado de UI\n- Providers de servicios\n\n### Almacenes (`lib/data/store/`)\n\nAlmacenamiento local basado en Hive:\n\n- Almacén de servidores\n- Almacén de ajustes\n- Almacén de caché\n\n## Capa de Vista (`lib/view/`)\n\n### Páginas (`lib/view/page/`)\n\nPantallas principales de la aplicación:\n\n- `server/` - Páginas de gestión de servidores\n- `ssh/` - Páginas de terminal SSH\n- `container/` - Páginas de contenedores\n- `setting/` - Páginas de ajustes\n- `storage/` - Páginas de SFTP\n- `snippet/` - Páginas de fragmentos (snippets)\n\n### Widgets (`lib/view/widget/`)\n\nComponentes de UI reutilizables:\n\n- Tarjetas de servidor\n- Gráficos de estado\n- Componentes de entrada\n- Diálogos\n\n## Archivos Generados\n\n- `lib/generated/l10n/` - Localización auto-generada\n- `*.g.dart` - Código generado (json_serializable, freezed, hive, riverpod)\n- `*.freezed.dart` - Clases inmutables de Freezed\n\n## Directorio de Paquetes (`/packages/`)\n\nContiene ramas (forks) personalizadas de las dependencias:\n\n- `dartssh2/` - Librería SSH\n- `xterm/` - Emulador de terminal\n- `fl_lib/` - Utilidades compartidas\n- `fl_build/` - Sistema de compilación\n"
  },
  {
    "path": "docs/src/content/docs/es/development/testing.md",
    "content": "---\ntitle: Pruebas\ndescription: Estrategias de prueba y ejecución de pruebas\n---\n\n## Ejecución de Pruebas\n\n```bash\n# Ejecutar todas las pruebas\nflutter test\n\n# Ejecutar un archivo de prueba específico\nflutter test test/battery_test.dart\n\n# Ejecutar con cobertura\nflutter test --coverage\n```\n\n## Estructura de las Pruebas\n\nLas pruebas se encuentran en el directorio `test/` reflejando la estructura de lib:\n\n```\ntest/\n├── data/\n│   ├── model/\n│   └── provider/\n├── view/\n│   └── widget/\n└── test_helpers.dart\n```\n\n## Pruebas Unitarias\n\nProbar la lógica de negocio y los modelos de datos:\n\n```dart\ntest('debería calcular el porcentaje de CPU', () {\n  final cpu = CpuModel(usage: 75.0);\n  expect(cpu.usagePercentage, '75%');\n});\n```\n\n## Pruebas de Widgets\n\nProbar componentes de la interfaz de usuario (UI):\n\n```dart\ntestWidgets('ServerCard muestra el nombre del servidor', (tester) async {\n  await tester.pumpWidget(\n    ProviderScope(\n      child: MaterialApp(\n        home: ServerCard(server: testServer),\n      ),\n    ),\n  );\n\n  expect(find.text('Test Server'), findsOneWidget);\n});\n```\n\n## Pruebas de Providers\n\nProbar providers de Riverpod:\n\n```dart\ntest('serverStatusProvider devuelve el estado', () async {\n  final container = ProviderContainer();\n  final status = await container.read(serverStatusProvider(testServer).future);\n  expect(status, isA<StatusModel>());\n});\n```\n\n## Mocking (Simulaciones)\n\nUtilizar mocks para dependencias externas:\n\n```dart\nclass MockSshService extends Mock implements SshService {}\n\ntest('se conecta al servidor', () async {\n  final mockSsh = MockSshService();\n  when(mockSsh.connect(any)).thenAnswer((_) async => true);\n\n  // Probar con el mock\n});\n```\n\n## Pruebas de Integración\n\nProbar flujos de usuario completos (en `integration_test/`):\n\n```dart\ntestWidgets('flujo de agregar servidor', (tester) async {\n  await tester.pumpWidget(MyApp());\n\n  // Tocar el botón de agregar\n  await tester.tap(find.byIcon(Icons.add));\n  await tester.pumpAndSettle();\n\n  // Completar el formulario\n  await tester.enterText(find.byKey(Key('name')), 'Test Server');\n  // ...\n});\n```\n\n## Buenas Prácticas\n\n1. **Arrange-Act-Assert**: Estructurar las pruebas claramente.\n2. **Nombres descriptivos**: Los nombres de las pruebas deben describir el comportamiento.\n3. **Una aserción por prueba**: Mantener las pruebas enfocadas.\n4. **Simular dependencias externas**: No depender de servidores reales.\n5. **Probar casos límite**: Listas vacías, valores nulos, etc.\n"
  },
  {
    "path": "docs/src/content/docs/es/index.mdx",
    "content": "---\ntitle: Server Box\ndescription: Una aplicación integral de gestión de servidores multiplataforma\nhero:\n  tagline: Administra tus servidores Linux desde cualquier lugar\n  actions:\n    - text: Empezar\n      link: /es/introduction/\n      icon: right-arrow\n      variant: primary\n    - text: Ver en GitHub\n      link: https://github.com/lollipopkit/flutter_server_box\n      icon: github\n      variant: minimal\n---\n\nimport { Card, CardGrid } from '@astrojs/starlight/components';\n\n## Características\n\n<CardGrid stagger>\n\t<Card title=\"Monitoreo en Tiempo Real\" icon=\"chart\">\n\t\tMonitorea CPU, memoria, disco, red, GPU y temperatura con hermosos gráficos en tiempo real.\n\t</Card>\n\t<Card title=\"Terminal SSH\" icon=\"terminal\">\n\t\tTerminal SSH con todas las funciones, soporte para múltiples pestañas y teclado virtual para dispositivos móviles.\n\t</Card>\n\t<Card title=\"Navegador de Archivos SFTP\" icon=\"folder\">\n\t\tAdministra archivos en tus servidores con el cliente SFTP integrado y el navegador de archivos local.\n\t</Card>\n\t<Card title=\"Gestión de Docker\" icon=\"box\">\n\t\tInicia, detén y monitorea contenedores Docker con una interfaz intuitiva.\n\t</Card>\n\t<Card title=\"Multiplataforma\" icon=\"device-mobile\">\n\t\tDisponible en iOS, Android, macOS, Linux, Windows y watchOS.\n\t</Card>\n\t<Card title=\"Más de 12 Idiomas\" icon=\"globe\">\n\t\tSoporte completo de localización que incluye inglés, chino, alemán, francés y más.\n\t</Card>\n</CardGrid>\n\n## Enlaces Rápidos\n\n- **Descarga**: Disponible en [App Store](https://apps.apple.com/app/id1586449703), [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) y [F-Droid](https://f-droid.org/)\n- **Documentación**: Explora las guías para comenzar con Server Box\n- **Soporte**: Únete a nuestra comunidad en GitHub para discusiones y problemas\n"
  },
  {
    "path": "docs/src/content/docs/es/installation.mdx",
    "content": "---\ntitle: Instalación\ndescription: Descarga e instala Server Box en tu dispositivo\n---\n\nServer Box está disponible en múltiples plataformas. Elige tu método de instalación preferido.\n\n## Aplicaciones Móviles\n\n### iOS\n\nDescárgalo desde la **[App Store](https://apps.apple.com/app/id1586449703)**.\n\n### Android\n\nElige tu fuente preferida:\n\n- **[F-Droid](https://f-droid.org/)** - Para usuarios que prefieren fuentes exclusivamente FOSS (Software Libre y de Código Abierto)\n- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** - Para la última versión directamente desde la fuente\n\n## Aplicaciones de Escritorio\n\n### macOS\n\nDescárgalo desde **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\nCaracterísticas:\n- Integración nativa con la barra de menú\n- Soporte para Intel y Apple Silicon\n\n### Linux\n\nDescárgalo desde **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\nDisponible en paquetes AppImage, deb o tar.gz.\n\n### Windows\n\nDescárgalo desde **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\n## watchOS\n\nDisponible en la **[App Store](https://apps.apple.com/app/id1586449703)** como parte de la aplicación para iOS.\n\n## Compilación desde el Código Fuente\n\nPara compilar Server Box desde el código fuente, consulta la sección de [Compilación](/es/development/building/) en la documentación de desarrollo.\n\n## Información de Versión\n\nConsulta la página de [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) para ver la última versión y el registro de cambios.\n"
  },
  {
    "path": "docs/src/content/docs/es/introduction.mdx",
    "content": "---\ntitle: Introducción\ndescription: Aprende qué es Server Box y qué puede hacer\n---\n\nServer Box es una aplicación integral de gestión de servidores multiplataforma creada con Flutter. Te permite monitorear, gestionar y controlar tus servidores Linux, Unix y Windows desde cualquier lugar.\n\n## ¿Qué es Server Box?\n\nServer Box proporciona una interfaz unificada para tareas de administración de servidores a través de conexiones SSH. Ya seas un administrador de sistemas, desarrollador o entusiasta con servidores domésticos, esta aplicación pone potentes herramientas de gestión de servidores en tu bolsillo.\n\n## Capacidades Clave\n\n- **Monitoreo en Tiempo Real**: Sigue el uso de CPU, memoria, disco, velocidad de red, estado de GPU y temperaturas del sistema.\n- **Terminal SSH**: Acceso total a la terminal con soporte multi-pestaña y apariencia personalizable.\n- **Cliente SFTP**: Explora y gestiona archivos en tus servidores.\n- **Gestión de Docker**: Controla contenedores con facilidad.\n- **Gestión de Procesos**: Visualiza y gestiona procesos del sistema.\n- **Servicios Systemd**: Inicia, detén y monitorea servicios systemd.\n- **Herramientas de Red**: Pruebas iPerf, ping y Wake-on-LAN.\n- **Snippets**: Guarda y ejecuta comandos de shell personalizados.\n\n## Plataformas Soportadas\n\nServer Box es verdaderamente multiplataforma:\n\n- **Móvil**: iOS y Android\n- **Escritorio**: macOS, Linux y Windows\n\n## Licencia\n\nEste proyecto está bajo la licencia AGPL v3. El código fuente está disponible en [GitHub](https://github.com/lollipopkit/flutter_server_box).\n"
  },
  {
    "path": "docs/src/content/docs/es/platforms/desktop.md",
    "content": "---\ntitle: Funciones de Escritorio\ndescription: Funciones específicas para macOS, Linux y Windows\n---\n\nServer Box en plataformas de escritorio ofrece funciones de productividad adicionales.\n\n## macOS\n\n### Integración en la Barra de Menús\n\n- Estado rápido del servidor en la barra de menús\n- Acceso al servidor con un solo clic\n- Modo compacto para una mínima distracción\n- Estilo nativo de la barra de menús de macOS\n\n### Persistencia del Estado de la Ventana\n\n- Recuerda la posición y el tamaño de la ventana\n- Restaura la sesión anterior al iniciar\n- Soporte para múltiples monitores\n\n### Funciones Nativas\n\n- **Barra de título**: Opción de barra de título personalizada o del sistema\n- **Modo pantalla completa**: Monitorización dedicada del servidor\n- **Atajos de teclado**: Atajos nativos de macOS\n- **Touch Bar** (dispositivos compatibles): Acciones rápidas\n\n## Linux\n\n### Integración Nativa\n\n- Soporte para bandeja del sistema (systray)\n- Integración con notificaciones de escritorio\n- Integración con el selector de archivos\n\n### Gestión de Ventanas\n\n- Soporte para X11 y Wayland\n- Compatible con gestores de ventanas en mosaico (tiling)\n- Opción de decoraciones de ventana personalizadas\n\n## Windows\n\n### Funciones\n\n- Integración en la bandeja del sistema\n- Acciones rápidas en la Jump List\n- Controles de ventana nativos\n- Opción de inicio automático al arrancar\n\n## Funciones de Escritorio Multiplataforma\n\n### Atajos de Teclado\n\n- **Cmd/Ctrl + N**: Nuevo servidor\n- **Cmd/Ctrl + W**: Cerrar pestaña\n- **Cmd/Ctrl + T**: Nueva pestaña de terminal\n- **Cmd/Ctrl + ,**: Ajustes\n\n### Temas\n\n- Tema claro\n- Tema oscuro\n- Tema AMOLED (negro puro)\n- Tema del sistema (sigue al SO)\n\n### Múltiples Ventanas\n\n- Abrir varios servidores en ventanas separadas\n- Arrastrar pestañas a una nueva ventana\n- Comparar estadísticas de servidores en paralelo\n\n### Ventajas sobre el Móvil\n\n- Pantalla más grande para monitorización\n- Teclado completo para la terminal\n- Operaciones de archivos más rápidas\n- Mejor multitarea\n"
  },
  {
    "path": "docs/src/content/docs/es/platforms/mobile.md",
    "content": "---\ntitle: Funciones Móviles\ndescription: Funciones específicas para iOS y Android\n---\n\nServer Box proporciona varias funciones específicas para dispositivos móviles iOS y Android.\n\n## Autenticación Biométrica\n\nAsegura tus servidores con autenticación biométrica:\n\n- **iOS**: Face ID o Touch ID\n- **Android**: Autenticación por huella dactilar\n\nActívalo en Ajustes > Seguridad > Autenticación biométrica.\n\n## Widgets de Pantalla de Inicio\n\nAñade widgets de estado del servidor a tu pantalla de inicio para una monitorización rápida.\n\n### iOS\n\n- Mantén pulsada la pantalla de inicio\n- Toca en **+** para añadir un widget\n- Busca \"Server Box\"\n- Elige el tamaño del widget:\n  - Pequeño: Estado de un solo servidor\n  - Mediano: Múltiples servidores\n  - Grande: Información detallada\n\n### Android\n\n- Mantén pulsada la pantalla de inicio\n- Toca en **Widgets**\n- Busca \"Server Box\"\n- Selecciona el tipo de widget\n\n## Ejecución en Segundo Plano\n\n### Android\n\nMantén las conexiones activas en segundo plano:\n\n- Actívalo en Ajustes > Avanzado > Ejecución en segundo plano\n- Requiere exclusión de la optimización de batería\n- Notificaciones persistentes para conexiones activas\n\n### iOS\n\nSe aplican limitaciones de segundo plano:\n\n- Las conexiones pueden pausarse en segundo plano\n- Reconexión rápida al volver a la app\n- Soporte para actualización en segundo plano\n\n## Notificaciones Push\n\nRecibe notificaciones para:\n\n- Alertas de servidor fuera de línea\n- Avisos de alto uso de recursos\n- Alertas de finalización de tareas\n\nConfigúralo en Ajustes > Notificaciones.\n\n## Funciones de UI Móvil\n\n- **Deslizar para refrescar**: Actualiza el estado del servidor\n- **Acciones de deslizamiento**: Operaciones rápidas de servidor\n- **Modo horizontal**: Mejor experiencia de terminal\n- **Teclado virtual**: Atajos de terminal\n\n## Integración de Archivos\n\n- **App Archivos (iOS)**: Acceso directo SFTP desde Archivos\n- **Storage Access Framework (Android)**: Comparte archivos con otras apps\n- **Selector de documentos**: Selección de archivos sencilla\n"
  },
  {
    "path": "docs/src/content/docs/es/principles/architecture.md",
    "content": "---\ntitle: Descripción General de la Arquitectura\ndescription: Arquitectura de alto nivel de la aplicación\n---\n\nServer Box sigue una arquitectura por capas con una clara separación de responsabilidades.\n\n## Capas de la Arquitectura\n\n```\n┌─────────────────────────────────────────────────┐\n│           Capa de Presentación (UI)             │\n│          lib/view/page/, lib/view/widget/       │\n│  - Páginas, Widgets, Controladores              │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│           Capa de Lógica de Negocio             │\n│              lib/data/provider/                 │\n│  - Riverpod Providers, State Notifiers          │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│            Capa de Acceso a Datos               │\n│         lib/data/store/, lib/data/model/        │\n│  - Hive Stores, Modelos de Datos                │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│          Capa de Integración Externa            │\n│  - SSH (dartssh2), Terminal (xterm), SFTP       │\n│  - Código específico de plataforma (iOS, etc.)  │\n└─────────────────────────────────────────────────┘\n```\n\n## Fundamentos de la Aplicación\n\n### Punto de Entrada Principal\n\n`lib/main.dart` inicializa la aplicación:\n\n```dart\nvoid main() {\n  runApp(\n    ProviderScope(\n      child: MyApp(),\n    ),\n  );\n}\n```\n\n### Widget Raíz\n\n`MyApp` proporciona:\n- **Gestión de Temas**: Cambio entre tema claro/oscuro\n- **Configuración de Rutas**: Estructura de navegación\n- **Provider Scope**: Raíz para la inyección de dependencias\n\n### Página de Inicio\n\n`HomePage` sirve como núcleo de navegación:\n- **Interfaz de Pestañas**: Servidor, Snippet, Contenedor, SSH\n- **Gestión de Estado**: Estado por pestaña\n- **Navegación**: Acceso a funciones\n\n## Sistemas Principales\n\n### Gestión de Estado: Riverpod\n\n**¿Por qué Riverpod?**\n- Seguridad en tiempo de compilación\n- Facilidad para realizar pruebas\n- Sin dependencia del Build context\n- Funciona en todas las plataformas\n\n**Tipos de Provider Utilizados:**\n- `StateProvider`: Estado mutable simple\n- `AsyncNotifierProvider`: Estados de carga/error/datos\n- `StreamProvider`: Flujos de datos en tiempo real\n- Future providers: Operaciones asíncronas únicas\n\n### Persistencia de Datos: Hive CE\n\n**¿Por qué Hive CE?**\n- Sin dependencias de código nativo\n- Almacenamiento clave-valor rápido\n- Tipado seguro con generación de código\n- Sin necesidad de anotaciones manuales de campos\n\n**Almacenes (Stores):**\n- `SettingStore`: Preferencias de la app\n- `ServerStore`: Configuraciones de servidores\n- `SnippetStore`: Fragmentos de comandos\n- `KeyStore`: Claves SSH\n\n### Modelos Inmutables: Freezed\n\n**Beneficios:**\n- Inmutabilidad en tiempo de compilación\n- Tipos Union para el estado\n- Serialización JSON integrada\n- Extensiones CopyWith\n\n## Estrategia Multiplataforma\n\n### Sistema de Plugins\n\nLos plugins de Flutter proporcionan la integración con la plataforma:\n\n| Plataforma | Método de Integración |\n|------------|-----------------------|\n| iOS | CocoaPods, Swift/Obj-C |\n| Android | Gradle, Kotlin/Java |\n| macOS | CocoaPods, Swift |\n| Linux | CMake, C++ |\n| Windows | CMake, C# |\n\n### Funciones Específicas por Plataforma\n\n**Solo iOS:**\n- Widgets de pantalla de inicio\n- Actividades en Directo (Live Activities)\n- Compañero de Apple Watch\n\n**Solo Android:**\n- Servicio en segundo plano\n- Notificaciones push\n- Acceso al sistema de archivos\n\n**Solo Escritorio:**\n- Integración en la barra de menús\n- Múltiples ventanas\n- Barra de título personalizada\n\n## Dependencias Personalizadas\n\n### Rama (Fork) de dartssh2\n\nCliente SSH mejorado con:\n- Mejor soporte para móviles\n- Gestión de errores mejorada\n- Optimizaciones de rendimiento\n\n### Rama (Fork) de xterm.dart\n\nEmulador de terminal con:\n- Renderizado optimizado para móviles\n- Soporte para gestos táctiles\n- Integración con teclado virtual\n\n### fl_lib\n\nPaquete de utilidades compartidas con:\n- Widgets comunes\n- Extensiones\n- Funciones de ayuda\n\n## Sistema de Compilación\n\n### Paquete fl_build\n\nSistema de compilación personalizado para:\n- Compilaciones multiplataforma\n- Firma de código\n- Empaquetado de recursos (assets)\n- Gestión de versiones\n\n### Proceso de Compilación\n\n```\nmake.dart (versión) → fl_build (compilación) → Salida de plataforma\n```\n\n1. **Pre-compilación**: Cálculo de la versión desde Git\n2. **Compilación**: Compilar para la plataforma de destino\n3. **Post-compilación**: Empaquetado y firma\n\n## Ejemplo de Flujo de Datos\n\n### Actualización del Estado del Servidor\n\n```\n1. El temporizador se activa →\n2. El Provider llama al servicio →\n3. El servicio ejecuta el comando SSH →\n4. La respuesta se analiza en el modelo →\n5. Se actualiza el estado →\n6. La UI se reconstruye con los nuevos datos\n```\n\n### Flujo de Acción del Usuario\n\n```\n1. El usuario toca un botón →\n2. El Widget llama al método del provider →\n3. El Provider actualiza el estado →\n4. El cambio de estado activa la reconstrucción →\n5. El nuevo estado se refleja en la UI\n```\n\n## Arquitectura de Seguridad\n\n### Protección de Datos\n\n- **Contraseñas**: Cifradas con flutter_secure_storage\n- **Claves SSH**: Cifradas en reposo\n- **Huellas de Host**: Almacenadas de forma segura\n- **Datos de Sesión**: No se persisten\n\n### Seguridad de Conexión\n\n- **Verificación de Clave de Host**: Detección de MITM\n- **Cifrado**: Cifrado SSH estándar\n- **Sin Texto Plano**: Los datos sensibles nunca se almacenan en plano\n"
  },
  {
    "path": "docs/src/content/docs/es/principles/sftp.md",
    "content": "---\ntitle: Sistema SFTP\ndescription: Cómo funciona el explorador de archivos SFTP\n---\n\nEl sistema SFTP proporciona capacidades de gestión de archivos sobre SSH.\n\n## Arquitectura\n\n```\n┌─────────────────────────────────────────────┐\n│              Capa UI de SFTP                │\n│  - Explorador de archivos (remoto)          │\n│  - Explorador de archivos (local)           │\n│  - Cola de transferencia                    │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          Gestión de Estado SFTP             │\n│  - sftpProvider                             │\n│  - Gestión de rutas                         │\n│  - Cola de operaciones                      │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         Capa de Protocolo SFTP              │\n│  - Subsistema SSH                           │\n│  - Operaciones de archivos                  │\n│  - Listado de directorios                   │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│            Transporte SSH                   │\n│  - Canal seguro                             │\n│  - Streaming de datos                       │\n└─────────────────────────────────────────────┘\n```\n\n## Establecimiento de la Conexión\n\n### Creación del Cliente SFTP\n\n```dart\nFuture<SftpClient> createSftpClient(Spi spi) async {\n  // 1. Obtener cliente SSH (reutilizar si está disponible)\n  final sshClient = await genClient(spi);\n\n  // 2. Abrir subsistema SFTP\n  final sftp = await sshClient.openSftp();\n\n  return sftp;\n}\n```\n\n### Reutilización de Conexiones\n\nSFTP reutiliza las conexiones SSH existentes:\n\n```dart\nclass ServerProvider {\n  SSHClient? _sshClient;\n  SftpClient? _sftpClient;\n\n  Future<SftpClient> getSftpClient(String spiId) async {\n    _sftpClient ??= await _sshClient!.openSftp();\n    return _sftpClient!;\n  }\n}\n```\n\n## Operaciones del Sistema de Archivos\n\n### Listado de Directorios\n\n```dart\nFuture<List<SftpFile>> listDirectory(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Listar directorio\n  final files = await sftp.listDir(path);\n\n  // Ordenar según ajustes\n  files.sort((a, b) {\n    switch (sortOption) {\n      case SortOption.name:\n        return a.name.toLowerCase().compareTo(b.name.toLowerCase());\n      case SortOption.size:\n        return a.size.compareTo(b.size);\n      case SortOption.time:\n        return a.modified.compareTo(b.modified);\n    }\n  });\n\n  // Carpetas primero si está activado\n  if (showFoldersFirst) {\n    final dirs = files.where((f) => f.isDirectory);\n    final regular = files.where((f) => !f.isDirectory);\n    return [...dirs, ...regular];\n  }\n\n  return files;\n}\n```\n\n### Metadatos de Archivo\n\n```dart\nclass SftpFile {\n  final String name;\n  final String path;\n  final int size;           // Bytes\n  final int modified;       // Timestamp Unix\n  final String permissions;  // ej., \"rwxr-xr-x\"\n  final String owner;\n  final String group;\n  final bool isDirectory;\n  final bool isSymlink;\n\n  String get sizeFormatted => formatBytes(size);\n  String get modifiedFormatted => formatDate(modified);\n}\n```\n\n## Operaciones de Archivo\n\n### Subida (Upload)\n\n```dart\nFuture<void> uploadFile(\n  String localPath,\n  String remotePath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Crear petición\n  final req = SftpReq(\n    spi: spi,\n    remotePath: remotePath,\n    localPath: localPath,\n    type: SftpReqType.upload,\n  );\n\n  // Añadir a la cola\n  _transferQueue.add(req);\n\n  // Ejecutar transferencia con progreso\n  final file = File(localPath);\n  final size = await file.length();\n  final stream = file.openRead();\n\n  await sftp.upload(\n    stream: stream,\n    toPath: remotePath,\n    onProgress: (transferred) {\n      _updateProgress(req, transferred, size);\n    },\n  );\n\n  // Completar\n  _transferQueue.remove(req);\n}\n```\n\n### Descarga (Download)\n\n```dart\nFuture<void> downloadFile(\n  String remotePath,\n  String localPath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Crear archivo local\n  final file = File(localPath);\n  final sink = file.openWrite();\n\n  // Descargar con progreso\n  final stat = await sftp.stat(remotePath);\n\n  await sftp.download(\n    fromPath: remotePath,\n    toSink: sink,\n    onProgress: (transferred) {\n      _updateProgress(\n        SftpReq(...),\n        transferred,\n        stat.size,\n      );\n    },\n  );\n\n  await sink.close();\n}\n```\n\n### Edición de Permisos\n\n```dart\nFuture<void> setPermissions(\n  String path,\n  String permissions,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Analizar permisos (ej., \"rwxr-xr-x\" o \"755\")\n  final mode = parsePermissions(permissions);\n\n  // Establecer vía comando SSH (más fiable que SFTP)\n  final ssh = await getSshClient(spiId);\n  await ssh.exec('chmod $mode \"$path\"');\n}\n```\n\n## Gestión de Rutas\n\n### Estructura de Rutas\n\n```dart\nclass PathWithPrefix {\n  final String prefix;  // ej., \"/home/user\"\n  final String path;    // Relativa o absoluta\n\n  String get fullPath {\n    if (path.startsWith('/')) {\n      return path;  // Ruta absoluta\n    }\n    return '$prefix/$path';  // Ruta relativa\n  }\n\n  PathWithPrefix cd(String subPath) {\n    return PathWithPrefix(\n      prefix: fullPath,\n      path: subPath,\n    );\n  }\n}\n```\n\n### Historial de Navegación\n\n```dart\nclass PathHistory {\n  final List<String> _history = [];\n  int _index = -1;\n\n  void push(String path) {\n    // Eliminar historial hacia adelante\n    _history.removeRange(_index + 1, _history.length);\n    _history.add(path);\n    _index = _history.length - 1;\n  }\n\n  String? back() {\n    if (_index > 0) {\n      _index--;\n      return _history[_index];\n    }\n    return null;\n  }\n\n  String? forward() {\n    if (_index < _history.length - 1) {\n      _index++;\n      return _history[_index];\n    }\n    return null;\n  }\n}\n```\n\n## Sistema de Transferencia\n\n### Petición de Transferencia\n\n```dart\nclass SftpReq {\n  final Spi spi;\n  final String remotePath;\n  final String localPath;\n  final SftpReqType type;\n  final DateTime createdAt;\n\n  int? totalBytes;\n  int? transferredBytes;\n  String? error;\n}\n```\n\n### Seguimiento de Progreso\n\n```dart\nclass TransferProgress {\n  final SftpReq request;\n  final int total;\n  final int transferred;\n  final DateTime startTime;\n\n  double get percentage => (transferred / total) * 100;\n  Duration get elapsed => DateTime.now().difference(startTime);\n\n  String get speedFormatted {\n    final bytesPerSecond = transferred / elapsed.inSeconds;\n    return formatSpeed(bytesPerSecond);\n  }\n}\n```\n\n### Gestión de Colas\n\n```dart\nclass TransferQueue {\n  final List<SftpReq> _queue = [];\n  final Map<String, TransferProgress> _progress = {};\n  int _concurrent = 3;  // Transferencias concurrentes máx.\n\n  Future<void> process() async {\n    final active = _progress.values.where((p) => p.isInProgress);\n    if (active.length >= _concurrent) return;\n\n    final pending = _queue.where((r) => !_progress.containsKey(r.id));\n    for (final req in pending.take(_concurrent - active.length)) {\n      _executeTransfer(req);\n    }\n  }\n\n  Future<void> _executeTransfer(SftpReq req) async {\n    try {\n      _progress[req.id] = TransferProgress.inProgress(req);\n\n      if (req.type == SftpReqType.upload) {\n        await uploadFile(req.localPath, req.remotePath);\n      } else {\n        await downloadFile(req.remotePath, req.localPath);\n      }\n\n      _progress[req.id] = TransferProgress.completed(req);\n    } catch (e) {\n      _progress[req.id] = TransferProgress.failed(req, e);\n    }\n  }\n}\n```\n\n## Patrón de Almacenamiento Local\n\n### Caché de Descargas\n\nLos archivos descargados se guardan en:\n\n```dart\nString getLocalDownloadPath(String spiId, String remotePath) {\n  final normalized = remotePath.replaceAll('/', '_');\n  return 'Paths.file/$spiId/$normalized';\n}\n```\n\nEjemplo:\n- Remoto: `/var/log/nginx/access.log`\n- spiId: `server-123`\n- Local: `Paths.file/server-123/_var_log_nginx_access.log`\n\n## Edición de Archivos\n\n### Flujo de Trabajo de Edición\n\n```dart\nFuture<void> editFile(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 1. Comprobar tamaño\n  final stat = await sftp.stat(path);\n  if (stat.size > editorMaxSize) {\n    showWarning('Archivo demasiado grande para el editor integrado');\n    return;\n  }\n\n  // 2. Descargar a temporal\n  final temp = await downloadToTemp(path);\n\n  // 3. Abrir en editor\n  final content = await openEditor(temp.path);\n\n  // 4. Subir de nuevo\n  await uploadFile(temp.path, path);\n\n  // 5. Limpieza\n  await temp.delete();\n}\n```\n\n### Integración con Editor Externo\n\n```dart\nFuture<void> editInExternalEditor(String path) async {\n  final ssh = await getSshClient(spiId);\n\n  // Abrir terminal con editor\n  final editor = getSetting('sftpEditor', 'vim');\n  await ssh.exec('$editor \"$path\"');\n\n  // El usuario edita en la terminal\n  // Tras guardar, refrescar la vista SFTP\n}\n```\n\n## Gestión de Errores\n\n### Errores de Permiso\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpPermissionException {\n  showError('Permiso denegado: ${stat.path}');\n  showHint('Comprueba los permisos y la propiedad del archivo');\n}\n```\n\n### Erreores de Conexión\n\n```dart\ntry {\n  await sftp.listDir(path);\n} on SftpConnectionException {\n  showError('Conexión perdida');\n  await reconnect();\n}\n```\n\n### Errores de Espacio\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpNoSpaceException {\n  showError('Disco lleno en el servidor remoto');\n}\n```\n\n## Optimizaciones de Rendimiento\n\n### Caché de Directorios\n\n```dart\nclass DirectoryCache {\n  final Map<String, CachedDirectory> _cache = {};\n  final Duration ttl = Duration(minutes: 5);\n\n  Future<List<SftpFile>> list(String path) async {\n    final cached = _cache[path];\n    if (cached != null && !cached.isExpired) {\n      return cached.files;\n    }\n\n    final files = await sftp.listDir(path);\n    _cache[path] = CachedDirectory(files);\n    return files;\n  }\n}\n```\n\n### Carga Perezosa (Lazy Loading)\n\nPara directorios grandes (>1000 elementos):\n\n```dart\nList<SftpFile> loadPage(String path, int page, int pageSize) {\n  final all = cache[path] ?? [];\n  final start = page * pageSize;\n  final end = start + pageSize;\n  return all.sublist(start, end.clamp(0, all.length));\n}\n```\n\n### Paginación\n\n```dart\nclass PaginatedDirectory {\n  static const pageSize = 100;\n\n  Future<List<SftpFile>> getPage(int page) async {\n    final offset = page * pageSize;\n    return await sftp.listDir(\n      path,\n      offset: offset,\n      limit: pageSize,\n    );\n  }\n}\n```\n"
  },
  {
    "path": "docs/src/content/docs/es/principles/ssh.md",
    "content": "---\ntitle: Conexión SSH\ndescription: Cómo se establecen y gestionan las conexiones SSH\n---\n\nEntendiendo las conexiones SSH en Server Box.\n\n## Flujo de Conexión\n\n```text\nEntrada de Usuario → Configuración Spi → genClient() → Cliente SSH → Sesión\n```\n\n### Paso 1: Configuración\n\nEl modelo `Spi` (Server Parameter Info) contiene:\n\n```dart\nclass Spi {\n  String id;         // ID del servidor\n  String name;       // Nombre del servidor\n  String ip;         // Dirección IP\n  int port;          // Puerto SSH (por defecto 22)\n  String user;       // Usuario\n  String? pwd;       // Contraseña (cifrada)\n  String? keyId;     // ID de la clave SSH\n  String? jumpId;    // ID del servidor de salto (Jump server)\n  String? alterUrl;  // URL alternativa\n}\n```\n\n### Paso 2: Generación del Cliente\n\n`genClient(spi)` crea el cliente SSH:\n\n```dart\nFuture<SSHClient> genClient(Spi spi) async {\n  // 1. Establecer socket\n  var socket = await connect(spi.ip, spi.port);\n\n  // 2. Probar URL alternativa si falla\n  if (socket == null && spi.alterUrl != null) {\n    socket = await connect(spi.alterUrl, spi.port);\n  }\n\n  if (socket == null) {\n    throw ConnectionException('Unable to connect');\n  }\n\n  // 3. Autenticar\n  final client = SSHClient(\n    socket: socket,\n    username: spi.user,\n    onPasswordRequest: () => spi.pwd,\n    onIdentityRequest: () => loadKey(spi.keyId),\n  );\n\n  // 4. Verificar clave de host\n  await verifyHostKey(client, spi);\n\n  return client;\n}\n```\n\n### Paso 3: Servidor de Salto (si está configurado)\n\nPara servidores de salto, conexión recursiva:\n\n```dart\nif (spi.jumpId != null) {\n  final jumpClient = await genClient(getJumpSpi(spi.jumpId));\n  final forwarded = await jumpClient.forwardLocal(\n    spi.ip,\n    spi.port,\n  );\n  // Conectar a través del socket reenviado\n}\n```\n\n## Métodos de Autenticación\n\n### Autenticación por Contraseña\n\n```dart\nonPasswordRequest: () => spi.pwd\n```\n\n- Contraseña almacenada cifrada en Hive\n- Descifrada al conectar\n- Enviada al servidor para verificación\n\n### Autenticación por Clave Privada\n\n```dart\nonIdentityRequest: () async {\n  final key = await KeyStore.get(spi.keyId);\n  return decyptPem(key.pem, key.password);\n}\n```\n\n**Proceso de Carga de Clave:**\n1. Recuperar clave cifrada de `KeyStore`\n2. Descifrar contraseña (biometría/aviso)\n3. Analizar formato PEM\n4. Estandarizar finales de línea (LF)\n5. Retornar para autenticación\n\n### Interacción por Teclado (Keyboard-Interactive)\n\n```dart\nonUserInfoRequest: (instructions) async {\n  // Gestionar desafío-respuesta\n  return responses;\n}\n```\n\nSoporta:\n- Autenticación por contraseña\n- Tokens OTP\n- Autenticación de doble factor (2FA)\n\n## Verificación de Clave de Host\n\n### ¿Por qué verificar las claves de host?\n\nEvita ataques de **Hombre en el Medio (MITM)** asegurando que te conectas al mismo servidor.\n\n### Formato de Almacenamiento\n\n```text\n{spi.id}::{keyType}\n```\n\nEjemplo:\n```text\nmi-servidor::ssh-ed25519\nmi-servidor::ecdsa-sha2-nistp256\n```\n\n### Formatos de Huella Digital (Fingerprint)\n\n**MD5 Hex:**\n```text\naa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99\n```\n\n**Base64:**\n```text\nSHA256:AbCdEf1234567890...=\n```\n\n### Flujo de Verificación\n\n```dart\nFuture<void> verifyHostKey(SSHClient client, Spi spi) async {\n  final key = await client.hostKey;\n  final keyType = key.type;\n  final fingerprint = md5Hex(key); // o base64\n\n  final stored = SettingStore.sshKnownHostsFingerprints\n      ['${spi.id}::$keyType'];\n\n  if (stored == null) {\n    // Nuevo host - preguntar al usuario\n    final trust = await promptUser(\n      'Host desconocido',\n      'Huella: $fingerprint',\n    );\n    if (trust) {\n      SettingStore.sshKnownHostsFingerprints\n          ['${spi.id}::$keyType'] = fingerprint;\n    }\n  } else if (stored != fingerprint) {\n    // Ha cambiado - advertir al usuario\n    await warnUser(\n      '¡La clave de host ha cambiado!',\n      'Posible ataque MITM',\n    );\n  }\n}\n```\n\n## Gestión de Sesiones\n\n### Pool de Conexiones\n\nClientes activos mantenidos en `ServerProvider`:\n\n```dart\nclass ServerProvider {\n  final Map<String, SSHClient> _clients = {};\n\n  SSHClient getClient(String spiId) {\n    return _clients[spiId] ??= connect(spiId);\n  }\n}\n```\n\n### Keep-Alive\n\nMantener la conexión durante la inactividad:\n\n```dart\nTimer.periodic(\n  Duration(seconds: 30),\n  (_) => client.sendKeepAlive(),\n);\n```\n\n### Reconexión Automática\n\nAl perder la conexión:\n\n```dart\nclient.onError.listen((error) async {\n  await Future.delayed(Duration(seconds: 5));\n  reconnect();\n});\n```\n\n## Ciclo de Vida de la Conexión\n\n```text\n┌─────────────┐\n│   Inicial   │\n└──────┬──────┘\n       │ connect()\n       ↓\n┌─────────────┐\n│ Conectando  │ ←──┐\n└──────┬──────┘   │\n       │ éxito    │\n       ↓          │ fallo (reintento)\n┌─────────────┐   │\n│ Conectado   │───┘\n└──────┬──────┘\n       │\n       ↓\n┌─────────────┐\n│   Activo    │ ──→ Enviar comandos\n└──────┬──────┘\n       │\n       ↓ (error/desconexión)\n┌─────────────┐\n│ Desconectado│\n└─────────────┘\n```\n\n## Gestión de Errores\n\n### Tiempo de Espera Agotado (Timeout)\n\n```dart\ntry {\n  await client.connect().timeout(\n    Duration(seconds: 30),\n  );\n} on TimeoutException {\n  throw ConnectionException('Tiempo de espera de conexión agotado');\n}\n```\n\n### Fallo de Autenticación\n\n```dart\nonAuthFail: (error) {\n  if (error.contains('password')) {\n    return 'Contraseña no válida';\n  } else if (error.contains('key')) {\n    return 'Clave SSH no válida';\n  }\n  return 'Fallo de autenticación';\n}\n```\n\n### Discrepancia en Clave de Host\n\n```dart\nonHostKeyMismatch: (stored, current) {\n  showSecurityWarning(\n    '¡La clave de host ha cambiado!',\n    'Posible ataque MITM',\n  );\n}\n```\n\n## Consideraciones de Rendimiento\n\n### Reutilización de Conexiones\n\n- Reutilizar clientes entre funciones\n- No desconectar/reconectar innecesariamente\n- Pool de conexiones para operaciones concurrentes\n\n### Ajustes Óptimos\n\n- **Timeout**: 30 segundos (ajustable)\n- **Keep-alive**: Cada 30 segundos\n- **Retraso de reintento**: 5 segundos\n\n### Eficiencia de Red\n\n- Conexión única para múltiples operaciones\n- Comandos en tubería (pipeline) cuando sea posible\n- Evitar abrir múltiples conexiones\n"
  },
  {
    "path": "docs/src/content/docs/es/principles/state.md",
    "content": "---\ntitle: Gestión de Estado\ndescription: Cómo se gestiona el estado con Riverpod\n---\n\nEntendiendo la arquitectura de gestión de estado en Server Box.\n\n## ¿Por qué Riverpod?\n\n**Beneficios Clave:**\n- **Seguridad en tiempo de compilación**: Detecta errores al compilar\n- **Sin necesidad de BuildContext**: Accede al estado desde cualquier lugar\n- **Facilidad de pruebas**: Sencillo de probar providers de forma aislada\n- **Generación de código**: Menos código repetitivo, tipado seguro\n\n## Arquitectura de Providers\n\n```\n┌─────────────────────────────────────────────┐\n│          Capa UI (Widgets)                  │\n│  - ConsumerWidget / ConsumerStatefulWidget  │\n│  - ref.watch() / ref.read()                 │\n└─────────────────────────────────────────────┘\n                ↓ observa (watches)\n┌─────────────────────────────────────────────┐\n│          Capa de Provider                   │\n│  - Anotaciones @riverpod                    │\n│  - Archivos *.g.dart generados              │\n└─────────────────────────────────────────────┘\n                ↓ usa (uses)\n┌─────────────────────────────────────────────┐\n│          Capa de Servicio / Store           │\n│  - Lógica de negocio                        │\n│  - Acceso a datos                           │\n└─────────────────────────────────────────────┘\n```\n\n## Tipos de Provider Utilizados\n\n### 1. StateProvider (Estado Simple)\n\nPara estados simples y observables:\n\n```dart\n@riverpod\nclass ThemeNotifier extends _$ThemeNotifier {\n  @override\n  ThemeMode build() {\n    // Cargar desde ajustes\n    return SettingStore.themeMode;\n  }\n\n  void setTheme(ThemeMode mode) {\n    state = mode;\n    SettingStore.themeMode = mode;  // Persistir\n  }\n}\n```\n\n**Uso:**\n```dart\nclass MyWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final theme = ref.watch(themeNotifierProvider);\n    return Text('Tema: $theme');\n  }\n}\n```\n\n### 2. AsyncNotifierProvider (Estado Asíncrono)\n\nPara datos que se cargan de forma asíncrona:\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    // Carga inicial\n    return await fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() async {\n      return await fetchStatus(server);\n    });\n  }\n}\n```\n\n**Uso:**\n```dart\nfinal status = ref.watch(serverStatusProvider(server));\n\nstatus.when(\n  data: (data) => StatusWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 3. StreamProvider (Datos en Tiempo Real)\n\nPara flujos de datos continuos:\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  final client = ref.watch(sshClientProvider(server));\n  final stream = client.monitorCpu();\n\n  // Liberación automática cuando no se observa\n  ref.onDispose(() {\n    client.stopMonitoring();\n  });\n\n  return stream;\n}\n```\n\n**Uso:**\n```dart\nfinal cpu = ref.watch(cpuUsageProvider(server));\n\ncpu.when(\n  data: (usage) => CpuChart(usage),\n  loading: () => CircularProgressIndicator(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 4. Family Providers (Parametrizados)\n\nProviders que aceptan parámetros:\n\n```dart\n@riverpod\nFuture<List<Container>> containers(ContainersRef ref, Server server) async {\n  final client = await ref.watch(sshClientProvider(server).future);\n  return await client.listContainers();\n}\n```\n\n**Uso:**\n```dart\nfinal containers = ref.watch(containersProvider(server));\n\n// Diferentes servidores = diferentes estados en caché\nfinal containers2 = ref.watch(containersProvider(server2));\n```\n\n## Optimizaciones de Rendimiento\n\n- **Provider Keep-Alive**: Usa `@Riverpod(keepAlive: true)` para evitar que se destruya automáticamente cuando no haya escuchadores.\n- **Observación selectiva**: Usa `select` para observar solo una parte específica del estado.\n- **Caché de Providers**: Los Family providers cachean resultados por parámetro.\n\n## Mejores Prácticas\n\n1. **Co-localizar providers**: Colócalos cerca de los widgets que los consumen.\n2. **Usar generación de código**: Usa siempre `@riverpod`.\n3. **Mantener providers enfocados**: Responsabilidad única.\n4. **Gestionar estados de carga**: Maneja siempre los estados de AsyncValue.\n5. **Liberar recursos**: Usa `ref.onDispose()` para la limpieza.\n6. **Evitar árboles de providers profundos**: Mantén el grafo de providers plano.\n"
  },
  {
    "path": "docs/src/content/docs/es/principles/terminal.md",
    "content": "---\ntitle: Implementación de la Terminal\ndescription: Cómo funciona internamente la terminal SSH\n---\n\nLa terminal SSH es una de las funciones más complejas, construida sobre un fork personalizado de xterm.dart.\n\n## Resumen de la Arquitectura\n\n```\n┌─────────────────────────────────────────────┐\n│          Capa de UI de la Terminal          │\n│  - Gestión de pestañas                      │\n│  - Teclado virtual                          │\n│  - Selección de texto                       │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│           Emulador xterm.dart               │\n│  - PTY (Pseudo Terminal)                    │\n│  - Emulación VT100/ANSI                     │\n│  - Motor de renderizado                     │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│            Capa de Cliente SSH              │\n│  - Sesión SSH                               │\n│  - Gestión de canales                       │\n│  - Streaming de datos                       │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│             Servidor Remoto                 │\n│  - Proceso de Shell                         │\n│  - Ejecución de comandos                    │\n└─────────────────────────────────────────────┘\n```\n\n## Ciclo de Vida de la Sesión de Terminal\n\n### 1. Creación de la Sesión\n\n```dart\nFuture<TerminalSession> createSession(Spi spi) async {\n  // 1. Obtener cliente SSH\n  final client = await genClient(spi);\n\n  // 2. Crear PTY\n  final pty = await client.openPty(\n    term: 'xterm-256color',\n    cols: 80,\n    rows: 24,\n  );\n\n  // 3. Inicializar emulador de terminal\n  final terminal = Terminal(\n    backend: PtyBackend(pty),\n  );\n\n  // 4. Configurar manejador de cambio de tamaño\n  terminal.onResize.listen((size) {\n    pty.resize(size.cols, size.rows);\n  });\n\n  return TerminalSession(\n    terminal: terminal,\n    pty: pty,\n    client: client,\n  );\n}\n```\n\n### 2. Emulación de Terminal\n\nEl fork de xterm.dart proporciona:\n\n**Emulación VT100/ANSI:**\n- Movimiento del cursor\n- Colores (soporte para 256 colores)\n- Atributos de texto (negrita, subrayado, etc.)\n- Regiones de desplazamiento\n- Búfer de pantalla alternativo\n\n**Renderizado:**\n- Renderizado basado en líneas\n- Soporte para texto bidireccional\n- Soporte para Unicode/emoji\n- Redibujado optimizado\n\n### 3. Flujo de Datos\n\n```\nEntrada del Usuario\n    ↓\nTeclado Virtual / Teclado Físico\n    ↓\nEmulador de Terminal (tecla → secuencia de escape)\n    ↓\nCanal SSH (envío)\n    ↓\nPTY Remoto\n    ↓\nShell Remoto\n    ↓\nSalida del Comando\n    ↓\nCanal SSH (recepción)\n    ↓\nEmulador de Terminal (analizar códigos ANSI)\n    ↓\nRenderizado en Pantalla\n```\n\n## Sistema de Múltiples Pestañas\n\n### Gestión de Pestañas\n\nLas pestañas mantienen su estado durante la navegación:\n- La conexión SSH se mantiene activa\n- Se preserva el estado de la terminal\n- Se mantiene el búfer de desplazamiento\n- Se retiene el historial de entrada\n\n## Teclado Virtual\n\n### Implementación Específica por Plataforma\n\n**iOS:**\n- Teclado personalizado basado en UIView\n- Conmutable con un botón de teclado\n- Mostrar/ocultar automáticamente basado en el enfoque\n\n**Android:**\n- Método de entrada personalizado\n- Integrado con el teclado del sistema\n- Botones de acción rápida\n\n### Botones del Teclado\n\n| Botón | Acción |\n|--------|--------|\n| **Conmutar** | Mostrar/ocultar teclado del sistema |\n| **Ctrl** | Enviar modificador Ctrl |\n| **Alt** | Enviar modificador Alt |\n| **SFTP** | Abrir directorio actual |\n| **Portapapeles** | Copiar/Pegar sensible al contexto |\n| **Snippets** | Ejecutar fragmento de código |\n\n## Selección de Texto\n\n1. **Pulsación larga**: Entrar en modo selección\n2. **Arrastrar**: Extender la selección\n3. **Soltar**: Copiar al portapapeles\n\n## Fuente y Dimensiones\n\n### Cálculo de Tamaño\n\n```dart\nclass TerminalDimensions {\n  static Size calculate(double fontSize, Size screenSize) {\n    final charWidth = fontSize * 0.6;  // Relación de aspecto monoespaciada\n    final charHeight = fontSize * 1.2;\n\n    final cols = (screenSize.width / charWidth).floor();\n    final rows = (screenSize.height / charHeight).floor();\n\n    return Size(cols.toDouble(), rows.toDouble());\n  }\n}\n```\n\n### Pellizcar para Ampliar (Pinch-to-Zoom)\n\n```dart\nGestureDetector(\n  onScaleStart: () => _baseFontSize = currentFontSize,\n  onScaleUpdate: (details) {\n    final newFontSize = _baseFontSize * details.scale;\n    resize(newFontSize);\n  },\n)\n```\n\n## Esquema de Colores\n\n- **Claro (Light)**: Fondo claro, texto oscuro\n- **Oscuro (Dark)**: Fondo oscuro, texto claro\n- **AMOLED**: Fondo negro puro\n\n## Optimizaciones de Rendimiento\n\n- **Dirty rectangle**: Solo redibujar las regiones cambiadas\n- **Caché de líneas**: Cachear las líneas renderizadas\n- **Desplazamiento perezoso (Lazy scrolling)**: Desplazamiento virtual para búferes largos\n- **Actualizaciones por lotes**: Unificar múltiples escrituras\n- **Compresión**: Comprimir el búfer de desplazamiento\n- **Debouncing**: Antirrebote para entradas rápidas\n"
  },
  {
    "path": "docs/src/content/docs/es/quick-start.mdx",
    "content": "---\ntitle: Inicio Rápido\ndescription: Comienza a usar Server Box en cuestión de minutos\n---\n\nSigue esta guía de inicio rápido para conectarte a tu primer servidor y comenzar la monitorización.\n\n## Paso 1: Agregar un Servidor\n\n1. Abre Server Box\n2. Toca el botón **+** para agregar un nuevo servidor\n3. Completa la información del servidor:\n   - **Nombre**: Un nombre descriptivo para tu servidor\n   - **Host**: Dirección IP o nombre de dominio\n   - **Puerto**: Puerto SSH (por defecto: 22)\n   - **Usuario**: Nombre de usuario SSH\n   - **Contraseña o Llave**: Método de autenticación\n\n4. Toca **Guardar** para agregar el servidor\n\n## Paso 2: Conectar y Monitorear\n\n1. Toca en la tarjeta de tu servidor para conectarte\n2. La aplicación establecerá una conexión SSH\n3. Verás el estado en tiempo real de:\n   - Uso de CPU\n   - Memoria (RAM) y Swap\n   - Uso de disco\n   - Velocidad de red\n\n## Paso 3: Explorar Funcionalidades\n\nUna vez conectado, puedes:\n\n- **Abrir la Terminal**: Toca el botón de la terminal para obtener acceso SSH completo\n- **Explorar Archivos**: Usa SFTP para gestionar archivos\n- **Gestionar Contenedores**: Visualiza y controla contenedores Docker\n- **Ver Procesos**: Revisa los procesos en ejecución\n- **Ejecutar Snippets**: Ejecuta comandos guardados\n\n## Consejos\n\n- **Autenticación Biométrica**: Activa Face ID / Touch ID / Huella dactilar para un acceso rápido (móvil)\n- **Widgets en la Pantalla de Inicio**: Agrega widgets de estado del servidor a tu pantalla de inicio (iOS/Android)\n- **Ejecución en Segundo Plano**: Mantén las conexiones activas en segundo plano (Android)\n"
  },
  {
    "path": "docs/src/content/docs/fr/advanced/bulk-import.md",
    "content": "---\ntitle: Importation massive de serveurs\ndescription: Importer plusieurs serveurs à partir d'un fichier JSON\n---\n\nImportez plusieurs configurations de serveur en une seule fois à l'aide d'un fichier JSON.\n\n## Format JSON\n\n:::danger[Avertissement de sécurité]\n**Ne stockez jamais de mots de passe en clair dans des fichiers !** Cet exemple JSON montre un champ de mot de passe à des fins de démonstration uniquement, mais vous devriez :\n\n- **Préférer les clés SSH** (`keyId`) au lieu de `pwd` - elles sont plus sûres\n- **Utiliser des gestionnaires de mots de passe** ou des variables d'environnement si vous devez utiliser des mots de passe\n- **Supprimer le fichier immédiatement** après l'importation - ne laissez pas traîner des identifiants\n- **Ajouter au .gitignore** - ne validez jamais de fichiers d'identifiants dans le contrôle de version\n:::\n\n```json\n[\n  {\n    \"name\": \"Mon serveur\",\n    \"ip\": \"example.com\",\n    \"port\": 22,\n    \"user\": \"root\",\n    \"pwd\": \"password\",\n    \"keyId\": \"\",\n    \"tags\": [\"production\"],\n    \"autoConnect\": false\n  }\n]\n```\n\n## Champs\n\n| Champ | Requis | Description |\n|-------|----------|-------------|\n| `name` | Oui | Nom d'affichage |\n| `ip` | Oui | Domaine ou adresse IP |\n| `port` | Oui | Port SSH (généralement 22) |\n| `user` | Oui | Nom d'utilisateur SSH |\n| `pwd` | Non | Mot de passe (à éviter - utilisez plutôt des clés SSH) |\n| `keyId` | Non | Nom de la clé SSH (à partir des clés privées - recommandé) |\n| `tags` | Non | Tags d'organisation |\n| `autoConnect` | Non | Connexion automatique au démarrage |\n\n## Étapes d'importation\n\n1. Créer un fichier JSON avec les configurations de serveur\n2. Paramètres → Sauvegarde → Importation massive de serveurs\n3. Sélectionnez votre fichier JSON\n4. Confirmez l'importation\n\n## Exemple\n\n```json\n[\n  {\n    \"name\": \"Production\",\n    \"ip\": \"prod.example.com\",\n    \"port\": 22,\n    \"user\": \"admin\",\n    \"keyId\": \"my-key\",\n    \"tags\": [\"production\", \"web\"]\n  },\n  {\n    \"name\": \"Développement\",\n    \"ip\": \"dev.example.com\",\n    \"port\": 2222,\n    \"user\": \"dev\",\n    \"keyId\": \"dev-key\",\n    \"tags\": [\"development\"]\n  }\n]\n```\n\n## Conseils\n\n- **Utilisez des clés SSH** au lieu de mots de passe lorsque cela est possible\n- **Testez la connexion** après l'importation\n- **Organisez avec des tags** pour une gestion plus facile\n- **Supprimez le fichier JSON** après l'importation\n- **Ne validez jamais** de fichiers JSON contenant des identifiants dans le contrôle de version\n"
  },
  {
    "path": "docs/src/content/docs/fr/advanced/custom-commands.md",
    "content": "---\ntitle: Commandes personnalisées\ndescription: Afficher la sortie des commandes personnalisées sur la page du serveur\n---\n\nAjoutez des commandes shell personnalisées pour afficher leur sortie sur la page de détails du serveur.\n\n## Configuration\n\n1. Paramètres du serveur → Commandes personnalisées\n2. Entrez les commandes au format JSON\n\n## Format de base\n\n```json\n{\n  \"Nom d'affichage\": \"commande shell\"\n}\n```\n\n**Exemple :**\n```json\n{\n  \"Mémoire\": \"free -h\",\n  \"Disque\": \"df -h\",\n  \"Uptime\": \"uptime\"\n}\n```\n\n## Visualisation des résultats\n\nAprès la configuration, les commandes personnalisées apparaissent sur la page de détails du serveur et s'actualisent automatiquement.\n\n## Noms de commandes spéciaux\n\n### server_card_top_right\n\nAffichage sur la carte du serveur de la page d'accueil (coin supérieur droit) :\n\n```json\n{\n  \"server_card_top_right\": \"votre-commande-ici\"\n}\n```\n\n## Conseils\n\n**Utilisez des chemins absolus :**\n```json\n{\"Mon script\": \"/usr/local/bin/mon-script.sh\"}\n```\n\n**Commandes avec pipe :**\n```json\n{\"Processus principal\": \"ps aux | sort -rk 3 | head -5\"}\n```\n\n**Formater la sortie :**\n```json\n{\"Charge CPU\": \"uptime | awk -F'load average:' '{print $2}'\"}\n```\n\n**Gardez les commandes rapides :** Moins de 5 secondes pour une meilleure expérience.\n\n**Limiter la sortie :**\n```json\n{\"Logs\": \"tail -20 /var/log/syslog\"}\n```\n\n## Sécurité\n\nLes commandes s'exécutent avec les permissions de l'utilisateur SSH. Évitez les commandes qui modifient l'état du système.\n"
  },
  {
    "path": "docs/src/content/docs/fr/advanced/custom-logo.md",
    "content": "---\ntitle: Logo de serveur personnalisé\ndescription: Utiliser des images personnalisées pour les cartes de serveur\n---\n\nAffichez des logos personnalisés sur les cartes de serveur à l'aide d'URL d'images.\n\n## Configuration\n\n1. Paramètres du serveur → Logo personnalisé\n2. Entrez l'URL de l'image\n\n## Espaces réservés d'URL\n\n### {DIST} - Distribution Linux\n\nRemplacé automatiquement par la distribution détectée :\n\n```\nhttps://example.com/{DIST}.png\n```\n\nDevient : `debian.png`, `ubuntu.png`, `arch.png`, etc.\n\n### {BRIGHT} - Thème\n\nRemplacé automatiquement par le thème actuel :\n\n```\nhttps://example.com/{BRIGHT}.png\n```\n\nDevient : `light.png` ou `dark.png`\n\n### Combiner les deux\n\n```\nhttps://example.com/{DIST}-{BRIGHT}.png\n```\n\nDevient : `debian-light.png`, `ubuntu-dark.png`, etc.\n\n## Conseils\n\n- Utilisez les formats PNG ou SVG\n- Taille recommandée : 64x64 à 128x128 pixels\n- Utilisez des URL HTTPS\n- Gardez des tailles de fichiers réduites\n\n## Distributions supportées\n\ndebian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt\n\nListe complète : [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)\n"
  },
  {
    "path": "docs/src/content/docs/fr/advanced/json-settings.md",
    "content": "---\ntitle: Paramètres cachés (JSON)\ndescription: Accéder aux paramètres avancés via l'éditeur JSON\n---\n\nCertains paramètres sont masqués de l'interface utilisateur mais accessibles via l'éditeur JSON.\n\n## Accès\n\nAppuyez longuement sur **Paramètres** dans le menu latéral pour ouvrir l'éditeur JSON.\n\n## Paramètres cachés courants\n\n### timeOut\n\nDélai d'attente de connexion en secondes.\n\n```json\n{\"timeOut\": 10}\n```\n\n**Type :** entier | **Par défaut :** 5 | **Plage :** 1-60\n\n### recordHistory\n\nEnregistrer l'historique (chemins SFTP, etc.).\n\n```json\n{\"recordHistory\": true}\n```\n\n**Type :** booléen | **Par défaut :** true\n\n### textFactor\n\nFacteur de mise à l'échelle du texte.\n\n```json\n{\"textFactor\": 1.2}\n```\n\n**Type :** double | **Par défaut :** 1.0 | **Plage :** 0.8-1.5\n\n## Trouver plus de paramètres\n\nTous les paramètres sont définis dans [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart).\n\nRecherchez :\n```dart\nlate final settingName = StoreProperty(box, 'settingKey', defaultValue);\n```\n\n## ⚠️ Important\n\n**Avant d'éditer :**\n- **Créer une sauvegarde** - De mauvais paramètres peuvent empêcher l'ouverture de l'application\n- **Éditer avec soin** - Le JSON doit être valide\n\n## Récupération\n\nSi l'application ne s'ouvre plus après l'édition :\n1. Effacer les données de l'application (dernier recours)\n2. Réinstaller l'application\n3. Restaurer à partir d'une sauvegarde\n"
  },
  {
    "path": "docs/src/content/docs/fr/advanced/troubleshooting.md",
    "content": "---\ntitle: Problèmes courants\ndescription: Solutions aux problèmes fréquents\n---\n\n## Problèmes de connexion\n\n### SSH ne se connecte pas\n\n**Symptômes :** Délai d'attente (timeout), connexion refusée, échec d'authentification\n\n**Solutions :**\n\n1. **Vérifier le type de serveur :** Seuls les systèmes de type Unix sont supportés (Linux, macOS, Android/Termux)\n2. **Tester manuellement :** `ssh utilisateur@serveur -p port`\n3. **Vérifier le pare-feu :** Le port 22 doit être ouvert\n4. **Vérifier les identifiants :** Nom d'utilisateur et mot de passe/clé corrects\n\n### Déconnexions fréquentes\n\n**Symptômes :** Le terminal se déconnecte après une période d'inactivité\n\n**Solutions :**\n\n1. **Keep-alive du serveur :**\n   ```bash\n   # /etc/ssh/sshd_config\n   ClientAliveInterval 60\n   ClientAliveCountMax 3\n   ```\n\n2. **Désactiver l'optimisation de la batterie :**\n   - MIUI : Batterie → \"Pas de restrictions\"\n   - Android : Paramètres → Applications → Désactiver l'optimisation\n   - iOS : Activer l'actualisation en arrière-plan\n\n## Problèmes de saisie\n\n### Impossible de taper certains caractères\n\n**Solution :** Paramètres → Type de clavier → Passer à `visiblePassword`\n\nNote : La saisie CJK (Chinois, Japonais, Coréen) peut ne pas fonctionner après ce changement.\n\n## Problèmes de l'application\n\n### L'application plante au démarrage\n\n**Symptômes :** L'application ne s'ouvre pas, écran noir\n\n**Causes :** Paramètres corrompus, particulièrement via l'éditeur JSON\n\n**Solutions :**\n\n1. **Effacer les données de l'application :**\n   - Android : Paramètres → Applications → ServerBox → Effacer les données\n   - iOS : Supprimer et réinstaller\n\n2. **Restaurer une sauvegarde :** Importer une sauvegarde créée avant de modifier les paramètres\n\n### Problèmes de sauvegarde/restauration\n\n**La sauvegarde ne fonctionne pas :**\n- Vérifier l'espace de stockage\n- Vérifier que l'application a les permissions de stockage\n- Essayer un autre emplacement\n\n**La restauration échoue :**\n- Vérifier l'intégrité du fichier de sauvegarde\n- Vérifier la compatibilité de la version de l'application\n\n## Problèmes de Widget\n\n### Le widget ne se met pas à jour\n\n**iOS :**\n- Attendre jusqu'à 30 minutes pour le rafraîchissement automatique\n- Supprimer et rajouter le widget\n- Vérifier que l'URL se termine par `/status`\n\n**Android :**\n- Appuyer sur le widget pour forcer le rafraîchissement\n- Vérifier que l'ID du widget correspond à la configuration dans les paramètres de l'application\n\n**watchOS :**\n- Redémarrer l'application sur la montre\n- Attendre quelques minutes après un changement de configuration\n- Vérifier le format de l'URL\n\n### Le widget affiche une erreur\n\n- Vérifier que ServerBox Monitor fonctionne sur le serveur\n- Tester l'URL dans un navigateur\n- Vérifier les identifiants d'authentification\n\n## Problèmes de performance\n\n### L'application est lente\n\n**Solutions :**\n- Réduire la fréquence de rafraîchissement dans les paramètres\n- Vérifier la vitesse du réseau\n- Désactiver les serveurs inutilisés\n\n### Utilisation élevée de la batterie\n\n**Solutions :**\n- Augmenter les intervalles de rafraîchissement\n- Désactiver le rafraîchissement en arrière-plan\n- Fermer les sessions SSH inutilisées\n\n## Obtenir de l'aide\n\nSi les problèmes persistent :\n\n1. **Rechercher dans les Issues GitHub :** https://github.com/lollipopkit/flutter_server_box/issues\n2. **Créer une nouvelle Issue :** Inclure la version de l'application, la plateforme et les étapes pour reproduire le problème\n3. **Consulter le Wiki :** Cette documentation et le Wiki GitHub\n"
  },
  {
    "path": "docs/src/content/docs/fr/advanced/widgets.md",
    "content": "---\ntitle: Widgets de l'écran d'accueil\ndescription: Ajoutez des widgets d'état du serveur à votre écran d'accueil\n---\n\nNécessite l'installation de [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) sur vos serveurs.\n\n## Prérequis\n\nInstallez d'abord ServerBox Monitor sur votre serveur. Consultez le [Wiki de ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor/wiki/Home) pour les instructions de configuration.\n\nAprès l'installation, votre serveur doit avoir :\n- Un point de terminaison HTTP/HTTPS\n- Un point de terminaison API `/status`\n- Une authentification facultative\n\n## Format de l'URL\n\n```\nhttps://votre-serveur.com/status\n```\n\nDoit se terminer par `/status`.\n\n## Widget iOS\n\n### Configuration\n\n1. Appuyez longuement sur l'écran d'accueil → Appuyez sur **+**\n2. Recherchez \"ServerBox\"\n3. Choisissez la taille du widget\n4. Appuyez longuement sur le widget → **Modifier le widget**\n5. Entrez l'URL se terminant par `/status`\n\n### Notes\n\n- Doit utiliser HTTPS (sauf pour les adresses IP locales)\n- Taux de rafraîchissement maximal : 30 minutes (limite iOS)\n- Ajoutez plusieurs widgets pour plusieurs serveurs\n\n## Widget Android\n\n### Configuration\n\n1. Appuyez longuement sur l'écran d'accueil → **Widgets**\n2. Trouvez \"ServerBox\" → Ajoutez à l'écran d'accueil\n3. Notez le numéro d'ID du widget affiché\n4. Ouvrez l'application ServerBox → Paramètres\n5. Appuyez sur **Configurer le lien du widget d'accueil**\n6. Ajoutez l'entrée : `Widget ID` = `URL d'état`\n\nExemple :\n- Clé : `17`\n- Valeur : `https://mon-serveur.com/status`\n\n7. Appuyez sur le widget sur l'écran d'accueil pour le rafraîchir\n\n## Widget watchOS\n\n### Configuration\n\n1. Ouvrez l'application iPhone → Paramètres\n2. **Paramètres iOS** → **Application Watch**\n3. Appuyez sur **Ajouter une URL**\n4. Entrez l'URL se terminant par `/status`\n5. Attendez que l'application de la montre se synchronise\n\n### Notes\n\n- Essayez de redémarrer l'application de la montre si elle ne se met pas à jour\n- Vérifiez que le téléphone et la montre sont connectés\n\n## Dépannage\n\n### Le widget ne se met pas à jour\n\n**iOS :** Attendez jusqu'à 30 minutes, puis supprimez et rajoutez-le.\n**Android :** Appuyez sur le widget pour forcer le rafraîchissement, vérifiez l'ID dans les paramètres.\n**watchOS :** Redémarrez l'application de la montre, attendez quelques minutes.\n\n### Le widget affiche une erreur\n\n- Vérifiez que ServerBox Monitor fonctionne\n- Testez l'URL dans un navigateur\n- Vérifiez que l'URL se termine par `/status`\n\n## Sécurité\n\n- **Utilisez toujours HTTPS** si possible\n- **Adresses IP locales uniquement** sur les réseaux de confiance\n"
  },
  {
    "path": "docs/src/content/docs/fr/development/architecture.md",
    "content": "---\ntitle: Architecture\ndescription: Modèles d'architecture et décisions de conception\n---\n\nServer Box suit les principes de la Clean Architecture avec une séparation claire entre les couches de données, de domaine et de présentation.\n\n## Architecture en couches\n\n```\n┌─────────────────────────────────────┐\n│          Couche Présentation        │\n│         (lib/view/page/)            │\n│  - Pages, Widgets, Contrôleurs      │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│         Couche Logique Métier       │\n│      (lib/data/provider/)           │\n│  - Providers Riverpod               │\n│  - Gestion de l'état                │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│           Couche Données            │\n│      (lib/data/model/, store/)      │\n│  - Modèles, Stockage, Services      │\n└─────────────────────────────────────┘\n```\n\n## Modèles clés\n\n### Gestion de l'état : Riverpod\n\n- **Génération de code** : Utilise `riverpod_generator` pour des providers type-safe\n- **State Notifiers** : Pour un état mutable avec une logique métier\n- **Async Notifiers** : Pour les états de chargement et d'erreur\n- **Stream Providers** : Pour les données en temps réel\n\n### Modèles immuables : Freezed\n\n- Tous les modèles de données utilisent Freezed pour l'immuabilité\n- Types Union pour la représentation de l'état\n- Sérialisation JSON intégrée\n- Extensions CopyWith pour les mises à jour\n\n### Stockage local : Hive\n\n- **hive_ce** : Édition communautaire de Hive\n- Pas de `@HiveField` ou `@HiveType` manuel requis\n- Adaptateurs de type auto-générés\n- Stockage clé-valeur persistant\n\n## Injection de dépendances\n\nLes services et les stores sont injectés via :\n\n1. **Providers** : Exposer les dépendances à l'UI\n2. **GetIt** : Localisation de services (le cas échéant)\n3. **Injection par constructeur** : Dépendances explicites\n\n## Flux de données\n\n```\nAction Utilisateur → Widget → Provider → Service/Store → Mise à jour Modèle → Reconstruction UI\n```\n\n1. L'utilisateur interagit avec le widget\n2. Le widget appelle une méthode du provider\n3. Le provider met à jour l'état via le service/store\n4. Le changement d'état déclenche la reconstruction de l'UI\n5. Le nouvel état est reflété dans le widget\n\n## Dépendances personnalisées\n\nLe projet utilise plusieurs forks personnalisés pour étendre les fonctionnalités :\n\n- **dartssh2** : Fonctionnalités SSH améliorées\n- **xterm** : Émulateur de terminal avec support mobile\n- **fl_lib** : Composants UI et utilitaires partagés\n\n## Threading (Multi-processus)\n\n- **Isolates** : Calculs lourds hors du thread principal\n- **paquet computer** : Utilitaires multi-threading\n- **Async/Await** : Opérations d'E/S non bloquantes\n"
  },
  {
    "path": "docs/src/content/docs/fr/development/building.md",
    "content": "---\ntitle: Construction (Building)\ndescription: Instructions de construction pour différentes plateformes\n---\n\nServer Box utilise un système de construction personnalisé (`fl_build`) pour les constructions multiplateformes.\n\n## Prérequis\n\n- Flutter SDK (canal stable)\n- Outils spécifiques à la plateforme (Xcode pour iOS, Android Studio pour Android)\n- Chaîne d'outils Rust (pour certaines dépendances natives)\n\n## Construction pour le développement\n\n```bash\n# Exécuter en mode développement\nflutter run\n\n# Exécuter sur un appareil spécifique\nflutter run -d <id-appareil>\n```\n\n## Construction pour la production\n\nLe projet utilise `fl_build` pour la construction :\n\n```bash\n# Construire pour une plateforme spécifique\ndart run fl_build -p <plateforme>\n\n# Plateformes disponibles :\n# - ios\n# - android\n# - macos\n# - linux\n# - windows\n```\n\n## Constructions spécifiques aux plateformes\n\n### iOS\n\n```bash\ndart run fl_build -p ios\n```\n\nNécessite :\n- macOS avec Xcode\n- CocoaPods\n- Compte Apple Developer pour la signature\n\n### Android\n\n```bash\ndart run fl_build -p android\n```\n\nNécessite :\n- Android SDK\n- Java Development Kit\n- Keystore pour la signature\n\n### macOS\n\n```bash\ndart run fl_build -p macos\n```\n\n### Linux\n\n```bash\ndart run fl_build -p linux\n```\n\n### Windows\n\n```bash\ndart run fl_build -p windows\n```\n\nNécessite Windows avec Visual Studio.\n\n## Pré/Post Construction\n\nLe script `make.dart` gère :\n\n- La génération des métadonnées\n- Les mises à jour de la chaîne de version\n- Les configurations spécifiques aux plateformes\n\n## Dépannage\n\n### Nettoyage de la construction (Clean Build)\n\n```bash\nflutter clean\ndart run build_runner build --delete-conflicting-outputs\nflutter pub get\n```\n\n### Incompatibilité de version\n\nAssurez-vous que toutes les dépendances sont compatibles :\n```bash\nflutter pub upgrade\n```\n\n## Liste de contrôle de publication (Release Checklist)\n\n1. Mettre à jour la version dans `pubspec.yaml`\n2. Exécuter la génération de code\n3. Exécuter les tests\n4. Construire pour toutes les plateformes cibles\n5. Tester sur des appareils physiques\n6. Créer une version (release) GitHub\n"
  },
  {
    "path": "docs/src/content/docs/fr/development/codegen.md",
    "content": "---\ntitle: Génération de code\ndescription: Utiliser build_runner pour la génération de code\n---\n\nServer Box utilise intensivement la génération de code pour les modèles, la gestion de l'état et la sérialisation.\n\n## Quand exécuter la génération de code\n\nÀ exécuter après avoir modifié :\n\n- Des modèles avec l'annotation `@freezed`\n- Des classes avec `@JsonSerializable`\n- Des modèles Hive\n- Des providers avec `@riverpod`\n- Des localisations (fichiers ARB)\n\n## Exécuter la génération de code\n\n```bash\n# Générer tout le code\ndart run build_runner build --delete-conflicting-outputs\n\n# Nettoyer et régénérer\ndart run build_runner build --delete-conflicting-outputs --clean\n```\n\n## Fichiers générés\n\n### Freezed (`*.freezed.dart`)\n\nModèles de données immuables avec types Union :\n\n```dart\n@freezed\nclass ServerState with _$ServerState {\n  const factory ServerState.connected() = Connected;\n  const factory ServerState.disconnected() = Disconnected;\n  const factory ServerState.error(String message) = Error;\n}\n```\n\n### Sérialisation JSON (`*.g.dart`)\n\nGénéré à partir de `json_serializable` :\n\n```dart\n@JsonSerializable()\nclass Server {\n  final String id;\n  final String name;\n  final String host;\n\n  Server({required this.id, required this.name, required this.host});\n\n  factory Server.fromJson(Map<String, dynamic> json) =>\n      _$ServerFromJson(json);\n  Map<String, dynamic> toJson() => _$ServerToJson(this);\n}\n```\n\n### Providers Riverpod (`*.g.dart`)\n\nGénéré à partir de l'annotation `@riverpod` :\n\n```dart\n@riverpod\nclass MyNotifier extends _$MyNotifier {\n  @override\n  int build() => 0;\n}\n```\n\n### Adaptateurs Hive (`*.g.dart`)\n\nAuto-générés pour les modèles Hive (hive_ce) :\n\n```dart\n@HiveType(typeId: 0)\nclass ServerModel {\n  @HiveField(0)\n  final String id;\n}\n```\n\n## Génération de localisation\n\n```bash\nflutter gen-l10n\n```\n\nGénère `lib/generated/l10n/` à partir des fichiers `lib/l10n/*.arb`.\n\n## Conseils\n\n- Utilisez `--delete-conflicting-outputs` pour éviter les conflits\n- Ajoutez les fichiers générés au `.gitignore`\n- Ne modifiez jamais manuellement les fichiers générés\n"
  },
  {
    "path": "docs/src/content/docs/fr/development/state.md",
    "content": "---\ntitle: Gestion de l'état\ndescription: Modèles de gestion de l'état basés sur Riverpod\n---\n\nServer Box utilise Riverpod avec la génération de code pour la gestion de l'état.\n\n## Types de Provider\n\n### StateProvider\n\nÉtat simple qui peut être lu et écrit :\n\n```dart\n@riverpod\nclass Settings extends _$Settings {\n  @override\n  SettingsModel build() {\n    return SettingsModel.defaults();\n  }\n\n  void update(SettingsModel newSettings) {\n    state = newSettings;\n  }\n}\n```\n\n### AsyncNotifierProvider\n\nÉtat qui se charge de manière asynchrone avec des états de chargement/erreur :\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    return fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() => fetchStatus(server));\n  }\n}\n```\n\n### StreamProvider\n\nDonnées en temps réel provenant de flux (streams) :\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  return cpuService.monitor(server);\n}\n```\n\n## Modèles d'état\n\n### États de chargement\n\n```dart\nstate.when(\n  data: (data) => DataWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### Family Providers\n\nProviders paramétrés :\n\n```dart\n@riverpod\nList<Container> containers(ContainersRef ref, Server server) {\n  return containerService.list(server);\n}\n```\n\n### Auto-Dispose\n\nProviders qui se détruisent lorsqu'ils ne sont plus référencés :\n\n```dart\n@Riverpod(keepAlive: false)\nclass TempState extends _$TempState {\n  // ...\n}\n```\n\n## Bonnes pratiques\n\n1. **Utiliser la génération de code** : Utilisez toujours l'annotation `@riverpod`\n2. **Co-localiser les providers** : Placez-les près des widgets qui les consomment\n3. **Éviter les singletons** : Utilisez des providers à la place\n4. **Couches correctes** : Gardez la logique UI séparée de la logique métier\n\n## Lire l'état dans les Widgets\n\n```dart\nclass ServerWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final status = ref.watch(serverStatusProvider(server));\n    return status.when(...);\n  }\n}\n```\n\n## Modifier l'état\n\n```dart\nref.read(settingsProvider.notifier).update(newSettings);\n```\n"
  },
  {
    "path": "docs/src/content/docs/fr/development/structure.md",
    "content": "---\ntitle: Structure du projet\ndescription: Comprendre la base de code de Server Box\n---\n\nLe projet Server Box suit une architecture modulaire avec une séparation claire des préoccupations.\n\n## Structure des répertoires\n\n```\nlib/\n├── core/              # Utilitaires de base et extensions\n├── data/              # Couche de données\n│   ├── model/         # Modèles de données par fonctionnalité\n│   ├── provider/      # Providers Riverpod\n│   └── store/         # Stockage local (Hive)\n├── view/              # Couche UI\n│   ├── page/          # Pages principales\n│   └── widget/        # Widgets réutilisables\n├── generated/         # Localisation générée\n├── l10n/              # Fichiers ARB de localisation\n└── hive/              # Adaptateurs Hive\n```\n\n## Couche Core (`lib/core/`)\n\nContient les utilitaires, les extensions et la configuration du routage :\n\n- **Extensions** : Extensions Dart pour les types courants\n- **Routes** : Configuration du routage de l'application\n- **Utils** : Fonctions utilitaires partagées\n\n## Couche Données (`lib/data/`)\n\n### Modèles (`lib/data/model/`)\n\nOrganisés par fonctionnalité :\n\n- `server/` - Modèles de connexion et d'état du serveur\n- `container/` - Modèles de conteneurs Docker\n- `ssh/` - Modèles de session SSH\n- `sftp/` - Modèles de fichiers SFTP\n- `app/` - Modèles spécifiques à l'application\n\n### Providers (`lib/data/provider/`)\n\nProviders Riverpod pour l'injection de dépendances et la gestion de l'état :\n\n- Providers de serveur\n- Providers d'état de l'UI\n- Providers de service\n\n### Stores (`lib/data/store/`)\n\nStockage local basé sur Hive :\n\n- Stockage des serveurs\n- Stockage des paramètres\n- Stockage du cache\n\n## Couche Vue (`lib/view/`)\n\n### Pages (`lib/view/page/`)\n\nÉcrans principaux de l'application :\n\n- `server/` - Pages de gestion des serveurs\n- `ssh/` - Pages de terminal SSH\n- `container/` - Pages de conteneurs\n- `setting/` - Pages de paramètres\n- `storage/` - Pages SFTP\n- `snippet/` - Pages d'extraits de code (snippets)\n\n### Widgets (`lib/view/widget/`)\n\nComposants UI réutilisables :\n\n- Cartes de serveur\n- Graphiques d'état\n- Composants de saisie (input)\n- Dialogues\n\n## Fichiers générés\n\n- `lib/generated/l10n/` - Localisation auto-générée\n- `*.g.dart` - Code généré (json_serializable, freezed, hive, riverpod)\n- `*.freezed.dart` - Classes immuables Freezed\n\n## Répertoire Packages (`/packages/`)\n\nContient les forks personnalisés des dépendances :\n\n- `dartssh2/` - Bibliothèque SSH\n- `xterm/` - Émulateur de terminal\n- `fl_lib/` - Utilitaires partagés\n- `fl_build/` - Système de construction\n"
  },
  {
    "path": "docs/src/content/docs/fr/development/testing.md",
    "content": "---\ntitle: Tests\ndescription: Stratégies de test et exécution des tests\n---\n\n## Exécuter les tests\n\n```bash\n# Exécuter tous les tests\nflutter test\n\n# Exécuter un fichier de test spécifique\nflutter test test/battery_test.dart\n\n# Exécuter avec couverture de code\nflutter test --coverage\n```\n\n## Structure des tests\n\nLes tests sont situés dans le répertoire `test/`, reflétant la structure de `lib/` :\n\n```\ntest/\n├── data/\n│   ├── model/\n│   └── provider/\n├── view/\n│   └── widget/\n└── test_helpers.dart\n```\n\n## Tests unitaires\n\nTester la logique métier et les modèles de données :\n\n```dart\ntest('devrait calculer le pourcentage du CPU', () {\n  final cpu = CpuModel(usage: 75.0);\n  expect(cpu.usagePercentage, '75%');\n});\n```\n\n## Tests de widgets\n\nTester les composants UI :\n\n```dart\ntestWidgets('ServerCard affiche le nom du serveur', (tester) async {\n  await tester.pumpWidget(\n    ProviderScope(\n      child: MaterialApp(\n        home: ServerCard(server: testServer),\n      ),\n    ),\n  );\n\n  expect(find.text('Test Server'), findsOneWidget);\n});\n```\n\n## Tests de providers\n\nTester les providers Riverpod :\n\n```dart\ntest('serverStatusProvider retourne le statut', () async {\n  final container = ProviderContainer();\n  final status = await container.read(serverStatusProvider(testServer).future);\n  expect(status, isA<StatusModel>());\n});\n```\n\n## Mocking (Simulations)\n\nUtiliser des mocks pour les dépendances externes :\n\n```dart\nclass MockSshService extends Mock implements SshService {}\n\ntest('se connecte au serveur', () async {\n  final mockSsh = MockSshService();\n  when(mockSsh.connect(any)).thenAnswer((_) async => true);\n\n  // Tester avec le mock\n});\n```\n\n## Tests d'intégration\n\nTester des flux utilisateurs complets (dans `integration_test/`) :\n\n```dart\ntestWidgets('flux d\\'ajout de serveur', (tester) async {\n  await tester.pumpWidget(MyApp());\n\n  // Appuyer sur le bouton d'ajout\n  await tester.tap(find.byIcon(Icons.add));\n  await tester.pumpAndSettle();\n\n  // Remplir le formulaire\n  await tester.enterText(find.byKey(Key('name')), 'Test Server');\n  // ...\n});\n```\n\n## Bonnes pratiques\n\n1. **Arrange-Act-Assert** : Structurer les tests clairement\n2. **Noms descriptifs** : Les noms de tests doivent décrire le comportement\n3. **Une assertion par test** : Garder les tests focalisés\n4. **Mocker les dépendances externes** : Ne pas dépendre de serveurs réels\n5. **Tester les cas limites** : Listes vides, valeurs nulles, etc.\n"
  },
  {
    "path": "docs/src/content/docs/fr/index.mdx",
    "content": "---\ntitle: Server Box\ndescription: Une application complète de gestion de serveurs multiplateforme\nhero:\n  tagline: Gérez vos serveurs Linux de n'importe où\n  actions:\n    - text: Commencer\n      link: /fr/introduction/\n      icon: right-arrow\n      variant: primary\n    - text: Voir sur GitHub\n      link: https://github.com/lollipopkit/flutter_server_box\n      icon: github\n      variant: minimal\n---\n\nimport { Card, CardGrid } from '@astrojs/starlight/components';\n\n## Fonctionnalités\n\n<CardGrid stagger>\n\t<Card title=\"Surveillance en temps réel\" icon=\"chart\">\n\t\tSurveillez le CPU, la mémoire, le disque, le réseau, le GPU et la température avec de magnifiques graphiques en temps réel.\n\t</Card>\n\t<Card title=\"Terminal SSH\" icon=\"terminal\">\n\t\tTerminal SSH complet avec support multi-onglets et clavier virtuel pour les appareils mobiles.\n\t</Card>\n\t<Card title=\"Navigateur de fichiers SFTP\" icon=\"folder\">\n\t\tGérez les fichiers sur vos serveurs avec le client SFTP intégré et le navigateur de fichiers local.\n\t</Card>\n\t<Card title=\"Gestion Docker\" icon=\"box\">\n\t\tDémarrez, arrêtez et surveillez les conteneurs Docker avec une interface intuitive.\n\t</Card>\n\t<Card title=\"Multiplateforme\" icon=\"device-mobile\">\n\t\tDisponible sur iOS, Android, macOS, Linux, Windows et watchOS.\n\t</Card>\n\t<Card title=\"Plus de 12 langues\" icon=\"globe\">\n\t\tSupport complet de localisation incluant l'anglais, le chinois, l'allemand, le français et plus encore.\n\t</Card>\n</CardGrid>\n\n## Liens rapides\n\n- **Téléchargement**: Disponible sur l'[App Store](https://apps.apple.com/app/id1586449703), [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) et [F-Droid](https://f-droid.org/)\n- **Documentation**: Explorez les guides pour commencer avec Server Box\n- **Support**: Rejoignez notre communauté sur GitHub pour des discussions et des problèmes\n"
  },
  {
    "path": "docs/src/content/docs/fr/installation.mdx",
    "content": "---\ntitle: Installation\ndescription: Téléchargez et installez Server Box sur votre appareil\n---\n\nServer Box est disponible sur plusieurs plateformes. Choisissez votre méthode d'installation préférée.\n\n## Applications Mobiles\n\n### iOS\n\nTéléchargez depuis l'**[App Store](https://apps.apple.com/app/id1586449703)**.\n\n### Android\n\nChoisissez votre source préférée :\n\n- **[F-Droid](https://f-droid.org/)** - Pour les utilisateurs qui préfèrent les sources exclusivement FOSS\n- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** - Pour la dernière version directement depuis la source\n\n## Applications de Bureau\n\n### macOS\n\nTéléchargez depuis les **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\nCaractéristiques :\n- Intégration native de la barre de menus\n- Prise en charge d'Intel et d'Apple Silicon\n\n### Linux\n\nTéléchargez depuis les **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\nDisponible en packages AppImage, deb ou tar.gz.\n\n### Windows\n\nTéléchargez depuis les **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\n## watchOS\n\nDisponible sur l'**[App Store](https://apps.apple.com/app/id1586449703)** en tant que partie de l'application iOS.\n\n## Construction à partir des sources\n\nPour construire Server Box à partir des sources, consultez la section [Construction](/fr/development/building/) dans la documentation de développement.\n\n## Informations sur la version\n\nConsultez la page [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) pour la dernière version et le journal des modifications (changelog).\n"
  },
  {
    "path": "docs/src/content/docs/fr/introduction.mdx",
    "content": "---\ntitle: Introduction\ndescription: Découvrez ce qu'est Server Box et ce qu'il peut faire\n---\n\nServer Box est une application complète de gestion de serveur multiplateforme construite avec Flutter. Elle vous permet de surveiller, gérer et contrôler vos serveurs Linux, Unix et Windows de n'importe où.\n\n## Qu'est-ce que Server Box ?\n\nServer Box fournit une interface unifiée pour les tâches d'administration de serveur via des connexions SSH. Que vous soyez un administrateur système, un développeur ou un passionné gérant des serveurs domestiques, cette application met de puissants outils de gestion de serveur dans votre poche.\n\n## Capacités clés\n\n- **Surveillance en temps réel** : Suivez le processeur (CPU), la mémoire, l'utilisation du disque, la vitesse du réseau, l'état du GPU et les températures du système.\n- **Terminal SSH** : Accès complet au terminal avec prise en charge multi-onglets et apparence personnalisable.\n- **Client SFTP** : Parcourez et gérez les fichiers sur vos serveurs.\n- **Gestion Docker** : Contrôlez les conteneurs en toute simplicité.\n- **Gestion des processus** : Visualisez et gérez les processus système.\n- **Services Systemd** : Démarrez, arrêtez et surveillez les services systemd.\n- **Outils réseau** : Tests iPerf, ping et Wake-on-LAN.\n- **Snippets** : Enregistrez et exécutez des commandes shell personnalisées.\n\n## Plateformes supportées\n\nServer Box est véritablement multiplateforme :\n\n- **Mobile** : iOS et Android\n- **Bureau** : macOS, Linux et Windows\n\n## Licence\n\nCe projet est sous licence AGPL v3. Le code source est disponible sur [GitHub](https://github.com/lollipopkit/flutter_server_box).\n"
  },
  {
    "path": "docs/src/content/docs/fr/platforms/desktop.md",
    "content": "---\ntitle: Fonctionnalités de bureau\ndescription: Fonctionnalités spécifiques à macOS, Linux et Windows\n---\n\nServer Box sur les plateformes de bureau offre des fonctionnalités de productivité supplémentaires.\n\n## macOS\n\n### Intégration de la barre de menus\n\n- État rapide du serveur dans la barre de menus\n- Accès au serveur en un clic\n- Mode compact pour une distraction minimale\n- Style de barre de menus natif macOS\n\n### Persistance de l'état des fenêtres\n\n- Mémorise la position et la taille de la fenêtre\n- Restaurer la session précédente au lancement\n- Prise en charge de plusieurs écrans\n\n### Fonctionnalités natives\n\n- **Barre de titre** : Option de barre de titre personnalisée ou système\n- **Mode plein écran** : Surveillance dédiée du serveur\n- **Raccourcis clavier** : Raccourcis natifs macOS\n- **Touch Bar** (appareils compatibles) : Actions rapides\n\n## Linux\n\n### Intégration native\n\n- Prise en charge de la zone de notification (systray)\n- Intégration des notifications de bureau\n- Intégration du sélecteur de fichiers\n\n### Gestion des fenêtres\n\n- Prise en charge de X11 et Wayland\n- Compatible avec les gestionnaires de fenêtres à tuiles (tiling)\n- Option de décorations de fenêtre personnalisées\n\n## Windows\n\n### Fonctionnalités\n\n- Intégration de la zone de notification (systray)\n- Actions rapides via la Jump List\n- Contrôles de fenêtre natifs\n- Option de démarrage automatique au boot\n\n## Fonctionnalités de bureau multiplateformes\n\n### Raccourcis clavier\n\n- **Cmd/Ctrl + N** : Nouveau serveur\n- **Cmd/Ctrl + W** : Fermer l'onglet\n- **Cmd/Ctrl + T** : Nouvel onglet de terminal\n- **Cmd/Ctrl + ,** : Paramètres\n\n### Thèmes\n\n- Thème clair\n- Thème sombre\n- Thème AMOLED (noir pur)\n- Thème système (suit l'OS)\n\n### Fenêtres multiples\n\n- Ouvrir plusieurs serveurs dans des fenêtres séparées\n- Faire glisser des onglets vers une nouvelle fenêtre\n- Comparer les statistiques des serveurs côte à côte\n\n### Avantages par rapport au mobile\n\n- Écran plus grand pour la surveillance\n- Clavier complet pour le terminal\n- Opérations de fichiers plus rapides\n- Meilleur multitâche\n"
  },
  {
    "path": "docs/src/content/docs/fr/platforms/mobile.md",
    "content": "---\ntitle: Fonctionnalités mobiles\ndescription: Fonctionnalités spécifiques à iOS et Android\n---\n\nServer Box offre plusieurs fonctionnalités spécifiques aux mobiles pour les appareils iOS et Android.\n\n## Authentification biométrique\n\nSécurisez vos serveurs avec l'authentification biométrique :\n\n- **iOS** : Face ID ou Touch ID\n- **Android** : Authentification par empreinte digitale\n\nActivez-la dans Paramètres > Sécurité > Authentification biométrique.\n\n## Widgets de l'écran d'accueil\n\nAjoutez des widgets d'état du serveur à votre écran d'accueil pour une surveillance rapide.\n\n### iOS\n\n- Appui long sur l'écran d'accueil\n- Appuyez sur **+** pour ajouter un widget\n- Recherchez \"Server Box\"\n- Choisissez la taille du widget :\n  - Petit : État d'un seul serveur\n  - Moyen : Plusieurs serveurs\n  - Grand : Informations détaillées\n\n### Android\n\n- Appui long sur l'écran d'accueil\n- Appuyez sur **Widgets**\n- Trouvez \"Server Box\"\n- Sélectionnez le type de widget\n\n## Fonctionnement en arrière-plan\n\n### Android\n\nMaintenir les connexions actives en arrière-plan :\n\n- Activer dans Paramètres > Avancé > Fonctionnement en arrière-plan\n- Nécessite l'exclusion de l'optimisation de la batterie\n- Notifications persistantes pour les connexions actives\n\n### iOS\n\nDes limitations en arrière-plan s'appliquent :\n\n- Les connexions peuvent se mettre en pause en arrière-plan\n- Reconnexion rapide au retour dans l'application\n- Prise en charge de l'actualisation en arrière-plan\n\n## Notifications Push\n\nRecevez des notifications pour :\n\n- Alertes de serveur hors ligne\n- Avertissements d'utilisation élevée des ressources\n- Alertes de fin de tâche\n\nConfigurez dans Paramètres > Notifications.\n\n## Fonctionnalités de l'interface mobile\n\n- **Tirer pour rafraîchir** : Mettre à jour l'état du serveur\n- **Actions de glissement** : Opérations rapides sur le serveur\n- **Mode paysage** : Meilleure expérience du terminal\n- **Clavier virtuel** : Raccourcis pour le terminal\n\n## Intégration de fichiers\n\n- **Application Fichiers (iOS)** : Accès direct SFTP depuis Fichiers\n- **Storage Access Framework (Android)** : Partager des fichiers avec d'autres applications\n- **Sélecteur de documents** : Sélection de fichiers facile\n"
  },
  {
    "path": "docs/src/content/docs/fr/principles/architecture.md",
    "content": "---\ntitle: Présentation de l'architecture\ndescription: Architecture de haut niveau de l'application\n---\n\nServer Box suit une architecture en couches avec une séparation claire des préoccupations.\n\n## Couches architecturales\n\n```\n┌─────────────────────────────────────────────────┐\n│          Couche de présentation (UI)            │\n│          lib/view/page/, lib/view/widget/       │\n│  - Pages, Widgets, Contrôleurs                   │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│         Couche logique métier                   │\n│         lib/data/provider/                      │\n│  - Riverpod Providers, State Notifiers          │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│           Couche d'accès aux données            │\n│         lib/data/store/, lib/data/model/        │\n│  - Hive Stores, Modèles de données              │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│         Couche d'intégration externe            │\n│  - SSH (dartssh2), Terminal (xterm), SFTP       │\n│  - Code spécifique à la plateforme (iOS, etc.)  │\n└─────────────────────────────────────────────────┘\n```\n\n## Fondations de l'application\n\n### Point d'entrée principal\n\n`lib/main.dart` initialise l'application :\n\n```dart\nvoid main() {\n  runApp(\n    ProviderScope(\n      child: MyApp(),\n    ),\n  );\n}\n```\n\n### Widget racine\n\n`MyApp` fournit :\n- **Gestion des thèmes** : Commutation entre thèmes clair/sombre\n- **Configuration du routage** : Structure de navigation\n- **Provider Scope** : Racine de l'injection de dépendances\n\n### Page d'accueil\n\n`HomePage` sert de plaque tournante pour la navigation :\n- **Interface par onglets** : Serveur, Snippet, Conteneur, SSH\n- **Gestion de l'état** : État par onglet\n- **Navigation** : Accès aux fonctionnalités\n\n## Systèmes de base\n\n### Gestion de l'état : Riverpod\n\n**Pourquoi Riverpod ?**\n- Sécurité au moment de la compilation\n- Tests faciles\n- Pas de dépendance au Build context\n- Fonctionne sur toutes les plateformes\n\n**Types de Provider utilisés :**\n- `StateProvider` : État mutable simple\n- `AsyncNotifierProvider` : États de chargement/erreur/données\n- `StreamProvider` : Flux de données en temps réel\n- Future providers : Opérations asynchrones uniques\n\n### Persistance des données : Hive CE\n\n**Pourquoi Hive CE ?**\n- Pas de dépendances de code natif\n- Stockage clé-valeur rapide\n- Type-safe avec génération de code\n- Pas d'annotations de champs manuelles requises\n\n**Stores :**\n- `SettingStore` : Préférences de l'application\n- `ServerStore` : Configurations de serveur\n- `SnippetStore` : Extraits de commande\n- `KeyStore` : Clés SSH\n\n### Modèles immuables : Freezed\n\n**Avantages :**\n- Immuabilité au moment de la compilation\n- Types Union pour l'état\n- Sérialisation JSON intégrée\n- Extensions CopyWith\n\n## Stratégie multiplateforme\n\n### Système de plugins\n\nLes plugins Flutter permettent l'intégration avec les plateformes :\n\n| Plateforme | Méthode d'intégration |\n|------------|----------------------|\n| iOS | CocoaPods, Swift/Obj-C |\n| Android | Gradle, Kotlin/Java |\n| macOS | CocoaPods, Swift |\n| Linux | CMake, C++ |\n| Windows | CMake, C# |\n\n### Fonctionnalités spécifiques aux plateformes\n\n**iOS uniquement :**\n- Widgets de l'écran d'accueil\n- Activités en direct (Live Activities)\n- Compagnon Apple Watch\n\n**Android uniquement :**\n- Service en arrière-plan\n- Notifications push\n- Accès au système de fichiers\n\n**Bureau uniquement :**\n- Intégration de la barre de menus\n- Fenêtres multiples\n- Barre de titre personnalisée\n\n## Dépendances personnalisées\n\n### Fork de dartssh2\n\nClient SSH amélioré avec :\n- Meilleur support mobile\n- Gestion des erreurs améliorée\n- Optimisations de performance\n\n### Fork de xterm.dart\n\nÉmulateur de terminal avec :\n- Rendu optimisé pour le mobile\n- Support des gestes tactiles\n- Intégration du clavier virtuel\n\n### fl_lib\n\nPaquet d'utilitaires partagés avec :\n- Widgets communs\n- Extensions\n- Fonctions d'aide\n\n## Système de construction\n\n### Paquet fl_build\n\nSystème de construction personnalisé pour :\n- Constructions multiplateformes\n- Signature de code\n- Regroupement des ressources (assets)\n- Gestion des versions\n\n### Processus de construction\n\n```\nmake.dart (version) → fl_build (build) → Sortie plateforme\n```\n\n1. **Pré-construction** : Calculer la version à partir de Git\n2. **Construction** : Compiler pour la plateforme cible\n3. **Post-construction** : Paqueter et signer\n\n## Exemple de flux de données\n\n### Mise à jour de l'état du serveur\n\n```\n1. Le minuteur se déclenche →\n2. Le Provider appelle le service →\n3. Le service exécute la commande SSH →\n4. La réponse est analysée en modèle →\n5. L'état est mis à jour →\n6. L'UI se reconstruit avec les nouvelles données\n```\n\n### Flux d'action utilisateur\n\n```\n1. L'utilisateur appuie sur un bouton →\n2. Le Widget appelle une méthode du provider →\n3. Le Provider met à jour l'état →\n4. Le changement d'état déclenche la reconstruction →\n5. Le nouvel état est reflété dans l'UI\n```\n\n## Architecture de sécurité\n\n### Protection des données\n\n- **Mots de passe** : Chiffrés avec flutter_secure_storage\n- **Clés SSH** : Chiffrées au repos\n- **Empreintes d'hôte** : Stockées de manière sécurisée\n- **Données de session** : Non persistées\n\n### Sécurité de la connexion\n\n- **Vérification de la clé d'hôte** : Détection MITM (homme du milieu)\n- **Chiffrement** : Chiffrement SSH standard\n- **Pas de texte clair** : Les données sensibles ne sont jamais stockées en clair\n"
  },
  {
    "path": "docs/src/content/docs/fr/principles/sftp.md",
    "content": "---\ntitle: Système SFTP\ndescription: Comment fonctionne le navigateur de fichiers SFTP\n---\n\nLe système SFTP fournit des capacités de gestion de fichiers via SSH.\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────┐\n│              Couche UI SFTP                 │\n│  - Navigateur de fichiers (distant)         │\n│  - Navigateur de fichiers (local)           │\n│  - File d'attente de transfert              │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          Gestion de l'état SFTP             │\n│  - sftpProvider                             │\n│  - Gestion des chemins                      │\n│  - File d'attente d'opérations              │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         Couche protocole SFTP               │\n│  - Sous-système SSH                         │\n│  - Opérations sur les fichiers              │\n│  - Liste des répertoires                    │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│            Transport SSH                    │\n│  - Canal sécurisé                           │\n│  - Streaming de données                     │\n└─────────────────────────────────────────────┘\n```\n\n## Établissement de la connexion\n\n### Création du client SFTP\n\n```dart\nFuture<SftpClient> createSftpClient(Spi spi) async {\n  // 1. Obtenir le client SSH (réutiliser si disponible)\n  final sshClient = await genClient(spi);\n\n  // 2. Ouvrir le sous-système SFTP\n  final sftp = await sshClient.openSftp();\n\n  return sftp;\n}\n```\n\n### Réutilisation de la connexion\n\nSFTP réutilise les connexions SSH existantes :\n\n```dart\nclass ServerProvider {\n  SSHClient? _sshClient;\n  SftpClient? _sftpClient;\n\n  Future<SftpClient> getSftpClient(String spiId) async {\n    _sftpClient ??= await _sshClient!.openSftp();\n    return _sftpClient!;\n  }\n}\n```\n\n## Opérations du système de fichiers\n\n### Liste des répertoires\n\n```dart\nFuture<List<SftpFile>> listDirectory(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Lister le répertoire\n  final files = await sftp.listDir(path);\n\n  // Trier selon les paramètres\n  files.sort((a, b) {\n    switch (sortOption) {\n      case SortOption.name:\n        return a.name.toLowerCase().compareTo(b.name.toLowerCase());\n      case SortOption.size:\n        return a.size.compareTo(b.size);\n      case SortOption.time:\n        return a.modified.compareTo(b.modified);\n    }\n  });\n\n  // Dossiers en premier si activé\n  if (showFoldersFirst) {\n    final dirs = files.where((f) => f.isDirectory);\n    final regular = files.where((f) => !f.isDirectory);\n    return [...dirs, ...regular];\n  }\n\n  return files;\n}\n```\n\n### Métadonnées de fichiers\n\n```dart\nclass SftpFile {\n  final String name;\n  final String path;\n  final int size;           // Octets\n  final int modified;       // Horodatage Unix\n  final String permissions;  // ex: \"rwxr-xr-x\"\n  final String owner;\n  final String group;\n  final bool isDirectory;\n  final bool isSymlink;\n\n  String get sizeFormatted => formatBytes(size);\n  String get modifiedFormatted => formatDate(modified);\n}\n```\n\n## Opérations sur les fichiers\n\n### Téléversement (Upload)\n\n```dart\nFuture<void> uploadFile(\n  String localPath,\n  String remotePath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Créer la requête\n  final req = SftpReq(\n    spi: spi,\n    remotePath: remotePath,\n    localPath: localPath,\n    type: SftpReqType.upload,\n  );\n\n  // Ajouter à la file d'attente\n  _transferQueue.add(req);\n\n  // Exécuter le transfert avec progression\n  final file = File(localPath);\n  final size = await file.length();\n  final stream = file.openRead();\n\n  await sftp.upload(\n    stream: stream,\n    toPath: remotePath,\n    onProgress: (transferred) {\n      _updateProgress(req, transferred, size);\n    },\n  );\n\n  // Terminé\n  _transferQueue.remove(req);\n}\n```\n\n### Téléchargement (Download)\n\n```dart\nFuture<void> downloadFile(\n  String remotePath,\n  String localPath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Créer le fichier local\n  final file = File(localPath);\n  final sink = file.openWrite();\n\n  // Télécharger avec progression\n  final stat = await sftp.stat(remotePath);\n\n  await sftp.download(\n    fromPath: remotePath,\n    toSink: sink,\n    onProgress: (transferred) {\n      _updateProgress(\n        SftpReq(...),\n        transferred,\n        stat.size,\n      );\n    },\n  );\n\n  await sink.close();\n}\n```\n\n### Édition des permissions\n\n```dart\nFuture<void> setPermissions(\n  String path,\n  String permissions,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Analyser les permissions (ex: \"rwxr-xr-x\" ou \"755\")\n  final mode = parsePermissions(permissions);\n\n  // Définir via commande SSH (plus fiable que SFTP)\n  final ssh = await getSshClient(spiId);\n  await ssh.exec('chmod $mode \"$path\"');\n}\n```\n\n## Gestion des chemins\n\n### Structure de chemin\n\n```dart\nclass PathWithPrefix {\n  final String prefix;  // ex: \"/home/user\"\n  final String path;    // Relatif ou absolu\n\n  String get fullPath {\n    if (path.startsWith('/')) {\n      return path;  // Chemin absolu\n    }\n    return '$prefix/$path';  // Chemin relatif\n  }\n\n  PathWithPrefix cd(String subPath) {\n    return PathWithPrefix(\n      prefix: fullPath,\n      path: subPath,\n    );\n  }\n}\n```\n\n### Historique de navigation\n\n```dart\nclass PathHistory {\n  final List<String> _history = [];\n  int _index = -1;\n\n  void push(String path) {\n    // Supprimer l'historique suivant\n    _history.removeRange(_index + 1, _history.length);\n    _history.add(path);\n    _index = _history.length - 1;\n  }\n\n  String? back() {\n    if (_index > 0) {\n      _index--;\n      return _history[_index];\n    }\n    return null;\n  }\n\n  String? forward() {\n    if (_index < _history.length - 1) {\n      _index++;\n      return _history[_index];\n    }\n    return null;\n  }\n}\n```\n\n## Système de transfert\n\n### Requête de transfert\n\n```dart\nclass SftpReq {\n  final Spi spi;\n  final String remotePath;\n  final String localPath;\n  final SftpReqType type;\n  final DateTime createdAt;\n\n  int? totalBytes;\n  int? transferredBytes;\n  String? error;\n}\n```\n\n### Suivi de progression\n\n```dart\nclass TransferProgress {\n  final SftpReq request;\n  final int total;\n  final int transferred;\n  final DateTime startTime;\n\n  double get percentage => (transferred / total) * 100;\n  Duration get elapsed => DateTime.now().difference(startTime);\n\n  String get speedFormatted {\n    final bytesPerSecond = transferred / elapsed.inSeconds;\n    return formatSpeed(bytesPerSecond);\n  }\n}\n```\n\n### Gestion de la file d'attente\n\n```dart\nclass TransferQueue {\n  final List<SftpReq> _queue = [];\n  final Map<String, TransferProgress> _progress = {};\n  int _concurrent = 3;  // Nombre max de transferts simultanés\n\n  Future<void> process() async {\n    final active = _progress.values.where((p) => p.isInProgress);\n    if (active.length >= _concurrent) return;\n\n    final pending = _queue.where((r) => !_progress.containsKey(r.id));\n    for (final req in pending.take(_concurrent - active.length)) {\n      _executeTransfer(req);\n    }\n  }\n\n  Future<void> _executeTransfer(SftpReq req) async {\n    try {\n      _progress[req.id] = TransferProgress.inProgress(req);\n\n      if (req.type == SftpReqType.upload) {\n        await uploadFile(req.localPath, req.remotePath);\n      } else {\n        await downloadFile(req.remotePath, req.localPath);\n      }\n\n      _progress[req.id] = TransferProgress.completed(req);\n    } catch (e) {\n      _progress[req.id] = TransferProgress.failed(req, e);\n    }\n  }\n}\n```\n\n## Modèle de stockage local\n\n### Cache de téléchargement\n\nFichiers téléchargés stockés sur :\n\n```dart\nString getLocalDownloadPath(String spiId, String remotePath) {\n  final normalized = remotePath.replaceAll('/', '_');\n  return 'Paths.file/$spiId/$normalized';\n}\n```\n\nExemple :\n- Distant : `/var/log/nginx/access.log`\n- spiId : `server-123`\n- Local : `Paths.file/server-123/_var_log_nginx_access.log`\n\n## Édition de fichiers\n\n### Flux d'édition\n\n```dart\nFuture<void> editFile(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 1. Vérifier la taille\n  final stat = await sftp.stat(path);\n  if (stat.size > editorMaxSize) {\n    showWarning('Fichier trop volumineux pour l\\'éditeur intégré');\n    return;\n  }\n\n  // 2. Télécharger vers dossier temp\n  final temp = await downloadToTemp(path);\n\n  // 3. Ouvrir dans l'éditeur\n  final content = await openEditor(temp.path);\n\n  // 4. Téléverser en retour\n  await uploadFile(temp.path, path);\n\n  // 5. Nettoyage\n  await temp.delete();\n}\n```\n\n### Intégration d'un éditeur externe\n\n```dart\nFuture<void> editInExternalEditor(String path) async {\n  final ssh = await getSshClient(spiId);\n\n  // Ouvrir le terminal avec l'éditeur\n  final editor = getSetting('sftpEditor', 'vim');\n  await ssh.exec('$editor \"$path\"');\n\n  // L'utilisateur édite dans le terminal\n  // Après sauvegarde, rafraîchir la vue SFTP\n}\n```\n\n## Gestion des erreurs\n\n### Erreurs de permission\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpPermissionException {\n  showError('Permission refusée : ${stat.path}');\n  showHint('Vérifiez les permissions et la propriété du fichier');\n}\n```\n\n### Erreurs de connexion\n\n```dart\ntry {\n  await sftp.listDir(path);\n} on SftpConnectionException {\n  showError('Connexion perdue');\n  await reconnect();\n}\n```\n\n### Erreurs d'espace disque\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpNoSpaceException {\n  showError('Disque plein sur le serveur distant');\n}\n```\n\n## Optimisations de performance\n\n### Cache de répertoire\n\n```dart\nclass DirectoryCache {\n  final Map<String, CachedDirectory> _cache = {};\n  final Duration ttl = Duration(minutes: 5);\n\n  Future<List<SftpFile>> list(String path) async {\n    final cached = _cache[path];\n    if (cached != null && !cached.isExpired) {\n      return cached.files;\n    }\n\n    final files = await sftp.listDir(path);\n    _cache[path] = CachedDirectory(files);\n    return files;\n  }\n}\n```\n\n### Chargement différé (Lazy Loading)\n\nPour les répertoires volumineux (>1000 éléments) :\n\n```dart\nList<SftpFile> loadPage(String path, int page, int pageSize) {\n  final all = cache[path] ?? [];\n  final start = page * pageSize;\n  final end = start + pageSize;\n  return all.sublist(start, end.clamp(0, all.length));\n}\n```\n\n### Pagination\n\n```dart\nclass PaginatedDirectory {\n  static const pageSize = 100;\n\n  Future<List<SftpFile>> getPage(int page) async {\n    final offset = page * pageSize;\n    return await sftp.listDir(\n      path,\n      offset: offset,\n      limit: pageSize,\n    );\n  }\n}\n```\n"
  },
  {
    "path": "docs/src/content/docs/fr/principles/ssh.md",
    "content": "---\ntitle: Connexion SSH\ndescription: Comment les connexions SSH sont établies et gérées\n---\n\nComprendre les connexions SSH dans Server Box.\n\n## Flux de connexion\n\n```text\nEntrée utilisateur → Configuration Spi → genClient() → Client SSH → Session\n```\n\n### Étape 1 : Configuration\n\nLe modèle `Spi` (Server Parameter Info) contient :\n\n```dart\nclass Spi {\n  String id;         // Identifiant unique\n  String name;       // Nom du serveur\n  String ip;         // Adresse IP\n  int port;          // Port SSH (par défaut 22)\n  String user;       // Nom d'utilisateur\n  String? pwd;       // Mot de passe (chiffré)\n  String? keyId;     // ID de la clé SSH\n  String? jumpId;    // ID du serveur de rebond (Jump server)\n  String? alterUrl;  // URL alternative\n}\n```\n\n### Étape 2 : Génération du client\n\n`genClient(spi)` crée le client SSH :\n\n```dart\nFuture<SSHClient> genClient(Spi spi) async {\n  // 1. Établir le socket\n  var socket = await connect(spi.ip, spi.port);\n\n  // 2. Essayer l'URL alternative en cas d'échec\n  if (socket == null && spi.alterUrl != null) {\n    socket = await connect(spi.alterUrl, spi.port);\n  }\n\n  if (socket == null) {\n    throw ConnectionException('Unable to connect');\n  }\n\n  // 3. Authentifier\n  final client = SSHClient(\n    socket: socket,\n    username: spi.user,\n    onPasswordRequest: () => spi.pwd,\n    onIdentityRequest: () => loadKey(spi.keyId),\n  );\n\n  // 4. Vérifier la clé d'hôte\n  await verifyHostKey(client, spi);\n\n  return client;\n}\n```\n\n### Étape 3 : Serveur de rebond (si configuré)\n\nPour les serveurs de rebond, connexion récursive :\n\n```dart\nif (spi.jumpId != null) {\n  final jumpClient = await genClient(getJumpSpi(spi.jumpId));\n  final forwarded = await jumpClient.forwardLocal(\n    spi.ip,\n    spi.port,\n  );\n  // Se connecter via le socket transféré\n}\n```\n\n## Méthodes d'authentification\n\n### Authentification par mot de passe\n\n```dart\nonPasswordRequest: () => spi.pwd\n```\n\n- Mot de passe stocké chiffré dans Hive\n- Déchiffré lors de la connexion\n- Envoyé au serveur pour vérification\n\n### Authentification par clé privée\n\n```dart\nonIdentityRequest: () async {\n  final key = await KeyStore.get(spi.keyId);\n  return decyptPem(key.pem, key.password);\n}\n```\n\n**Processus de chargement de la clé :**\n1. Récupérer la clé chiffrée depuis `KeyStore`\n2. Déchiffrer le mot de passe (biométrie/invite)\n3. Analyser le format PEM\n4. Standardiser les fins de ligne (LF)\n5. Retourner pour l'authentification\n\n### Keyboard-Interactive\n\n```dart\nonUserInfoRequest: (instructions) async {\n  // Gérer le challenge-response\n  return responses;\n}\n```\n\nSupporte :\n- L'authentification par mot de passe\n- Les jetons OTP\n- L'authentification à deux facteurs (2FA)\n\n## Vérification de la clé d'hôte\n\n### Pourquoi vérifier les clés d'hôte ?\n\nEmpêche les attaques de type **Man-in-the-Middle (MITM)** en s'assurant que vous vous connectez au même serveur.\n\n### Format de stockage\n\n```text\n{spi.id}::{keyType}\n```\n\nExemple :\n```text\nmon-serveur::ssh-ed25519\nmon-serveur::ecdsa-sha2-nistp256\n```\n\n### Formats d'empreinte\n\n**MD5 Hex :**\n```text\naa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99\n```\n\n**Base64 :**\n```text\nSHA256:AbCdEf1234567890...=\n```\n\n### Flux de vérification\n\n```dart\nFuture<void> verifyHostKey(SSHClient client, Spi spi) async {\n  final key = await client.hostKey;\n  final keyType = key.type;\n  final fingerprint = md5Hex(key); // ou base64\n\n  final stored = SettingStore.sshKnownHostsFingerprints\n      ['${spi.id}::$keyType'];\n\n  if (stored == null) {\n    // Nouvel hôte - inviter l'utilisateur\n    final trust = await promptUser(\n      'Hôte inconnu',\n      'Empreinte : $fingerprint',\n    );\n    if (trust) {\n      SettingStore.sshKnownHostsFingerprints\n          ['${spi.id}::$keyType'] = fingerprint;\n    }\n  } else if (stored != fingerprint) {\n    // Modifié - avertir l'utilisateur\n    await warnUser(\n      'La clé d\\'hôte a changé !',\n      'Attaque MITM possible',\n    );\n  }\n}\n```\n\n## Gestion des sessions\n\n### Mise en commun des connexions (Pooling)\n\nClients actifs maintenus dans `ServerProvider` :\n\n```dart\nclass ServerProvider {\n  final Map<String, SSHClient> _clients = {};\n\n  SSHClient getClient(String spiId) {\n    return _clients[spiId] ??= connect(spiId);\n  }\n}\n```\n\n### Keep-Alive\n\nMaintenir la connexion pendant l'inactivité :\n\n```dart\nTimer.periodic(\n  Duration(seconds: 30),\n  (_) => client.sendKeepAlive(),\n);\n```\n\n### Reconnexion automatique\n\nEn cas de perte de connexion :\n\n```dart\nclient.onError.listen((error) async {\n  await Future.delayed(Duration(seconds: 5));\n  reconnect();\n});\n```\n\n## Cycle de vie de la connexion\n\n```text\n┌─────────────┐\n│   Initial   │\n└──────┬──────┘\n       │ connect()\n       ↓\n┌─────────────┐\n│ Connexion   │ ←──┐\n└──────┬──────┘   │\n       │ succès   │\n       ↓          │ échec (retry)\n┌─────────────┐   │\n│  Connecté   │───┘\n└──────┬──────┘\n       │\n       ↓\n┌─────────────┐\n│    Actif    │ ──→ Envoyer des commandes\n└──────┬──────┘\n       │\n       ↓ (erreur/déconnexion)\n┌─────────────┐\n│ Déconnecté  │\n└─────────────┘\n```\n\n## Gestion des erreurs\n\n### Délai d'attente de connexion (Timeout)\n\n```dart\ntry {\n  await client.connect().timeout(\n    Duration(seconds: 30),\n  );\n} on TimeoutException {\n  throw ConnectionException('Délai d\\'attente de connexion dépassé');\n}\n```\n\n### Échec d'authentification\n\n```dart\nonAuthFail: (error) {\n  if (error.contains('password')) {\n    return 'Mot de passe invalide';\n  } else if (error.contains('key')) {\n    return 'Clé SSH invalide';\n  }\n  return 'Authentification échouée';\n}\n```\n\n### Discordance de clé d'hôte\n\n```dart\nonHostKeyMismatch: (stored, current) {\n  showSecurityWarning(\n    'La clé d\\'hôte a changé !',\n    'Attaque MITM possible',\n  );\n}\n```\n\n## Considérations de performance\n\n### Réutilisation de la connexion\n\n- Réutiliser les clients entre les fonctionnalités\n- Ne pas déconnecter/reconnecter inutilement\n- Mutualiser les connexions pour les opérations simultanées\n\n### Paramètres optimaux\n\n- **Timeout** : 30 secondes (ajustable)\n- **Keep-alive** : Toutes les 30 secondes\n- **Délai de relecture** : 5 secondes\n\n### Efficacité du réseau\n\n- Connexion unique pour plusieurs opérations\n- Commandes en pipeline si possible\n- Éviter d'ouvrir plusieurs connexions\n"
  },
  {
    "path": "docs/src/content/docs/fr/principles/state.md",
    "content": "---\ntitle: Gestion de l'état\ndescription: Comment l'état est géré avec Riverpod\n---\n\nComprendre l'architecture de gestion de l'état dans Server Box.\n\n## Pourquoi Riverpod ?\n\n**Avantages clés :**\n- **Sécurité à la compilation** : Capture les erreurs lors de la compilation\n- **Pas de BuildContext requis** : Accès à l'état n'importe où\n- **Tests faciles** : Simple de tester les providers de manière isolée\n- **Génération de code** : Moins de code répétitif, type-safe\n\n## Architecture des Providers\n\n```\n┌─────────────────────────────────────────────┐\n│         Couche UI (Widgets)                 │\n│  - ConsumerWidget / ConsumerStatefulWidget  │\n│  - ref.watch() / ref.read()                 │\n└─────────────────────────────────────────────┘\n                ↓ observe (watches)\n┌─────────────────────────────────────────────┐\n│         Couche Provider                     │\n│  - Annotations @riverpod                    │\n│  - Fichiers *.g.dart générés                │\n└─────────────────────────────────────────────┘\n                ↓ utilise (uses)\n┌─────────────────────────────────────────────┐\n│         Couche Service / Store              │\n│  - Logique métier                           │\n│  - Accès aux données                        │\n└─────────────────────────────────────────────┘\n```\n\n## Types de Providers utilisés\n\n### 1. StateProvider (État simple)\n\nPour un état simple et observable :\n\n```dart\n@riverpod\nclass ThemeNotifier extends _$ThemeNotifier {\n  @override\n  ThemeMode build() {\n    // Charger depuis les paramètres\n    return SettingStore.themeMode;\n  }\n\n  void setTheme(ThemeMode mode) {\n    state = mode;\n    SettingStore.themeMode = mode;  // Persister\n  }\n}\n```\n\n**Utilisation :**\n```dart\nclass MyWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final theme = ref.watch(themeNotifierProvider);\n    return Text('Thème : $theme');\n  }\n}\n```\n\n### 2. AsyncNotifierProvider (État asynchrone)\n\nPour les données qui se chargent de manière asynchrone :\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    // Chargement initial\n    return await fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() async {\n      return await fetchStatus(server);\n    });\n  }\n}\n```\n\n**Utilisation :**\n```dart\nfinal status = ref.watch(serverStatusProvider(server));\n\nstatus.when(\n  data: (data) => StatusWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 3. StreamProvider (Données en temps réel)\n\nPour les flux de données continus :\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  final client = ref.watch(sshClientProvider(server));\n  final stream = client.monitorCpu();\n\n  // Libération automatique des ressources quand non observé\n  ref.onDispose(() {\n    client.stopMonitoring();\n  });\n\n  return stream;\n}\n```\n\n**Utilisation :**\n```dart\nfinal cpu = ref.watch(cpuUsageProvider(server));\n\ncpu.when(\n  data: (usage) => CpuChart(usage),\n  loading: () => CircularProgressIndicator(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 4. Family Providers (Paramétrés)\n\nProviders qui acceptent des paramètres :\n\n```dart\n@riverpod\nFuture<List<Container>> containers(ContainersRef ref, Server server) async {\n  final client = await ref.watch(sshClientProvider(server).future);\n  return await client.listContainers();\n}\n```\n\n**Utilisation :**\n```dart\nfinal containers = ref.watch(containersProvider(server));\n\n// Différents serveurs = différents états mis en cache\nfinal containers2 = ref.watch(containersProvider(server2));\n```\n\n## Optimisations de performance\n\n- **Provider Keep-Alive** : Utilisez `@Riverpod(keepAlive: true)` pour empêcher la destruction automatique quand il n'y a plus d'écouteurs.\n- **Observation sélective** : Utilisez `select` pour n'observer qu'une partie spécifique de l'état.\n- **Mise en cache des Providers** : Les Family providers mettent en cache les résultats par paramètre.\n\n## Bonnes pratiques\n\n1. **Co-localiser les providers** : Placez-les près des widgets qui les consomment.\n2. **Utiliser la génération de code** : Utilisez toujours `@riverpod`.\n3. **Garder les providers focalisés** : Responsabilité unique.\n4. **Gérer les états de chargement** : Gérez toujours les états AsyncValue.\n5. **Libérer les ressources** : Utilisez `ref.onDispose()` pour le nettoyage.\n6. **Éviter les arbres de providers profonds** : Gardez le graphe des providers plat.\n"
  },
  {
    "path": "docs/src/content/docs/fr/principles/terminal.md",
    "content": "---\ntitle: Implémentation du terminal\ndescription: Comment le terminal SSH fonctionne en interne\n---\n\nLe terminal SSH est l'une des fonctionnalités les plus complexes, basée sur un fork personnalisé de xterm.dart.\n\n## Présentation de l'architecture\n\n```\n┌─────────────────────────────────────────────┐\n│          Couche UI du terminal              │\n│  - Gestion des onglets                      │\n│  - Clavier virtuel                          │\n│  - Sélection de texte                       │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         Émulateur xterm.dart                │\n│  - PTY (Pseudo Terminal)                    │\n│  - Émulation VT100/ANSI                     │\n│  - Moteur de rendu                          │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          Couche client SSH                  │\n│  - Session SSH                              │\n│  - Gestion des canaux                       │\n│  - Streaming de données                     │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          Serveur distant                    │\n│  - Processus Shell                          │\n│  - Exécution de commandes                   │\n└─────────────────────────────────────────────┘\n```\n\n## Cycle de vie d'une session de terminal\n\n### 1. Création de la session\n\n```dart\nFuture<TerminalSession> createSession(Spi spi) async {\n  // 1. Obtenir le client SSH\n  final client = await genClient(spi);\n\n  // 2. Créer le PTY\n  final pty = await client.openPty(\n    term: 'xterm-256color',\n    cols: 80,\n    rows: 24,\n  );\n\n  // 3. Initialiser l'émulateur de terminal\n  final terminal = Terminal(\n    backend: PtyBackend(pty),\n  );\n\n  // 4. Configurer le gestionnaire de redimensionnement\n  terminal.onResize.listen((size) {\n    pty.resize(size.cols, size.rows);\n  });\n\n  return TerminalSession(\n    terminal: terminal,\n    pty: pty,\n    client: client,\n  );\n}\n```\n\n### 2. Émulation de terminal\n\nLe fork xterm.dart fournit :\n\n**Émulation VT100/ANSI :**\n- Mouvement du curseur\n- Couleurs (support 256 couleurs)\n- Attributs de texte (gras, souligné, etc.)\n- Régions de défilement\n- Tampon d'écran alterné\n\n**Rendu :**\n- Rendu basé sur les lignes\n- Support du texte bidirectionnel\n- Support Unicode/emoji\n- Redessins optimisés\n\n### 3. Flux de données\n\n```\nEntrée utilisateur\n    ↓\nClavier virtuel / Clavier physique\n    ↓\nÉmulateur de terminal (touche → séquence d'échappement)\n    ↓\nCanal SSH (envoi)\n    ↓\nPTY distant\n    ↓\nShell distant\n    ↓\nSortie de commande\n    ↓\nCanal SSH (réception)\n    ↓\nÉmulateur de terminal (analyse des codes ANSI)\n    ↓\nRendu à l'écran\n```\n\n## Système multi-onglets\n\n### Gestion des onglets\n\nLes onglets maintiennent leur état lors de la navigation :\n- Connexion SSH maintenue active\n- État du terminal préservé\n- Tampon de défilement conservé\n- Historique de saisie retenu\n\n## Clavier virtuel\n\n### Implémentation spécifique à la plateforme\n\n**iOS :**\n- Clavier personnalisé basé sur UIView\n- Basculable avec un bouton clavier\n- Affichage/masquage automatique basé sur le focus\n\n**Android :**\n- Méthode de saisie personnalisée\n- Intégré au clavier système\n- Boutons d'action rapide\n\n### Boutons du clavier\n\n| Bouton | Action |\n|--------|--------|\n| **Basculer** | Afficher/masquer le clavier système |\n| **Ctrl** | Envoyer le modificateur Ctrl |\n| **Alt** | Envoyer le modificateur Alt |\n| **SFTP** | Ouvrir le répertoire courant |\n| **Presse-papiers** | Copier/Coller contextuel |\n| **Snippets** | Exécuter un extrait de code |\n\n## Sélection de texte\n\n1. **Appui long** : Entrer en mode sélection\n2. **Glisser** : Étendre la sélection\n3. **Relâcher** : Copier dans le presse-papiers\n\n## Police et dimensions\n\n### Calcul de la taille\n\n```dart\nclass TerminalDimensions {\n  static Size calculate(double fontSize, Size screenSize) {\n    final charWidth = fontSize * 0.6;  // Ratio d'aspect monospace\n    final charHeight = fontSize * 1.2;\n\n    final cols = (screenSize.width / charWidth).floor();\n    final rows = (screenSize.height / charHeight).floor();\n\n    return Size(cols.toDouble(), rows.toDouble());\n  }\n}\n```\n\n### Pincer pour zoomer (Pinch-to-Zoom)\n\n```dart\nGestureDetector(\n  onScaleStart: () => _baseFontSize = currentFontSize,\n  onScaleUpdate: (details) {\n    final newFontSize = _baseFontSize * details.scale;\n    resize(newFontSize);\n  },\n)\n```\n\n## Schéma de couleurs\n\n- **Clair (Light)** : Fond clair, texte sombre\n- **Sombre (Dark)** : Fond sombre, texte clair\n- **AMOLED** : Fond noir pur\n\n## Optimisations de performance\n\n- **Dirty rectangle** : Ne redessiner que les régions modifiées\n- **Mise en cache des lignes** : Mettre en cache les lignes rendues\n- **Défilement paresseux (Lazy scrolling)** : Défilement virtuel pour les longs tampons\n- **Mises à jour par lots** : Fusionner plusieurs écritures\n- **Compression** : Compresser le tampon de défilement\n- **Anti-rebond (Debouncing)** : Anti-rebond pour les saisies rapides\n"
  },
  {
    "path": "docs/src/content/docs/fr/quick-start.mdx",
    "content": "---\ntitle: Démarrage Rapide\ndescription: Soyez opérationnel avec Server Box en quelques minutes\n---\n\nSuivez ce guide de démarrage rapide pour vous connecter à votre premier serveur et commencer la surveillance.\n\n## Étape 1 : Ajouter un serveur\n\n1. Ouvrez Server Box\n2. Appuyez sur le bouton **+** pour ajouter un nouveau serveur\n3. Remplissez les informations du serveur :\n   - **Nom** : Un nom convivial pour votre serveur\n   - **Hôte** : Adresse IP ou nom de domaine\n   - **Port** : Port SSH (par défaut : 22)\n   - **Utilisateur** : Nom d'utilisateur SSH\n   - **Mot de passe ou Clé** : Méthode d'authentification\n\n4. Appuyez sur **Enregistrer** pour ajouter le serveur\n\n## Étape 2 : Connecter et surveiller\n\n1. Appuyez sur la carte de votre serveur pour vous connecter\n2. L'application établira une connexion SSH\n3. Vous verrez le statut en temps réel pour :\n   - L'utilisation du processeur (CPU)\n   - La mémoire (RAM) et le Swap\n   - L'utilisation du disque\n   - La vitesse du réseau\n\n## Étape 3 : Explorer les fonctionnalités\n\nUne fois connecté, vous pouvez :\n\n- **Ouvrir le terminal** : Appuyez sur le bouton du terminal pour un accès SSH complet\n- **Parcourir les fichiers** : Utilisez SFTP pour gérer les fichiers\n- **Gérer les conteneurs** : Visualisez et contrôlez les conteneurs Docker\n- **Afficher les processus** : Vérifiez les processus en cours d'exécution\n- **Exécuter des snippets** : Exécutez des commandes enregistrées\n\n## Conseils\n\n- **Authentification biométrique** : Activez Face ID / Touch ID / Empreinte digitale pour un accès rapide (mobile)\n- **Widgets de l'écran d'accueil** : Ajoutez des widgets d'état du serveur à votre écran d'accueil (iOS/Android)\n- **Fonctionnement en arrière-plan** : Maintenez les connexions actives en arrière-plan (Android)\n"
  },
  {
    "path": "docs/src/content/docs/index.mdx",
    "content": "---\ntitle: Server Box\ndescription: A comprehensive cross-platform server management application\nhero:\n  tagline: Manage your Linux servers from anywhere\n  actions:\n    - text: Get Started\n      link: /introduction/\n      icon: right-arrow\n      variant: primary\n    - text: View on GitHub\n      link: https://github.com/lollipopkit/flutter_server_box\n      icon: github\n      variant: minimal\n---\n\nimport { Card, CardGrid } from '@astrojs/starlight/components';\n\n## Features\n\n<CardGrid stagger>\n\t<Card title=\"Real-time Monitoring\" icon=\"chart\">\n\t\tMonitor CPU, memory, disk, network, GPU, and temperature with beautiful real-time charts.\n\t</Card>\n\t<Card title=\"SSH Terminal\" icon=\"terminal\">\n\t\tFull-featured SSH terminal with multi-tab support and virtual keyboard for mobile devices.\n\t</Card>\n\t<Card title=\"SFTP File Browser\" icon=\"folder\">\n\t\tManage files on your servers with the built-in SFTP client and local file browser.\n\t</Card>\n\t<Card title=\"Docker Management\" icon=\"box\">\n\t\tStart, stop, and monitor Docker containers with an intuitive interface.\n\t</Card>\n\t<Card title=\"Cross-Platform\" icon=\"device-mobile\">\n\t\tAvailable on iOS, Android, macOS, Linux, Windows, and watchOS.\n\t</Card>\n\t<Card title=\"12+ Languages\" icon=\"globe\">\n\t\tFull localization support including English, Chinese, German, French, and more.\n\t</Card>\n</CardGrid>\n\n## Quick Links\n\n- **Download**: Available on [App Store](https://apps.apple.com/app/id1586449703), [GitHub](https://github.com/lollipopkit/flutter_server_box/releases), and [F-Droid](https://f-droid.org/)\n- **Documentation**: Explore the guides to get started with Server Box\n- **Support**: Join our community on GitHub for discussions and issues\n"
  },
  {
    "path": "docs/src/content/docs/installation.mdx",
    "content": "---\ntitle: Installation\ndescription: Download and install Server Box on your device\n---\n\nServer Box is available on multiple platforms. Choose your preferred method of installation.\n\n## Mobile Apps\n\n### iOS\n\nDownload from the **[App Store](https://apps.apple.com/app/id1586449703)**.\n\n### Android\n\nChoose your preferred source:\n\n- **[F-Droid](https://f-droid.org/)** - For users who prefer FOSS-only sources\n- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** - For the latest version directly from the source\n\n## Desktop Apps\n\n### macOS\n\nDownload from **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\nFeatures:\n- Native menu bar integration\n- Support for both Intel and Apple Silicon\n\n### Linux\n\nDownload from **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\nAvailable as AppImage, deb, or tar.gz packages.\n\n### Windows\n\nDownload from **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.\n\n## watchOS\n\nAvailable on the **[App Store](https://apps.apple.com/app/id1586449703)** as part of the iOS app.\n\n## Building from Source\n\nTo build Server Box from source, see the [Building](/development/building) section in the Development documentation.\n\n## Version Information\n\nCheck the [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) page for the latest version and changelog.\n"
  },
  {
    "path": "docs/src/content/docs/introduction.mdx",
    "content": "---\ntitle: Introduction\ndescription: Learn what Server Box is and what it can do\n---\n\nServer Box is a comprehensive cross-platform server management application built with Flutter. It allows you to monitor, manage, and control your Linux, Unix, and Windows servers from anywhere.\n\n## What is Server Box?\n\nServer Box provides a unified interface for server administration tasks through SSH connections. Whether you're a system administrator, developer, or hobbyist running home servers, this app puts powerful server management tools in your pocket.\n\n## Key Capabilities\n\n- **Real-time Monitoring**: Track CPU, memory, disk usage, network speed, GPU status, and system temperatures\n- **SSH Terminal**: Full terminal access with multi-tab support and customizable appearance\n- **SFTP Client**: Browse and manage files on your servers\n- **Docker Management**: Control containers with ease\n- **Process Management**: View and manage system processes\n- **Systemd Services**: Start, stop, and monitor systemd services\n- **Network Tools**: iPerf testing, ping, and Wake-on-LAN\n- **Snippets**: Save and execute custom shell commands\n\n## Supported Platforms\n\nServer Box is truly cross-platform:\n\n- **Mobile**: iOS and Android\n- **Desktop**: macOS, Linux, and Windows\n\n## License\n\nThis project is licensed under AGPL v3. Source code is available on [GitHub](https://github.com/lollipopkit/flutter_server_box).\n"
  },
  {
    "path": "docs/src/content/docs/ja/advanced/bulk-import.md",
    "content": "---\ntitle: サーバーの一括インポート\ndescription: JSON ファイルから複数のサーバーをインポートする\n---\n\nJSON ファイルを使用して、複数のサーバー設定を一度にインポートします。\n\n## JSON 形式\n\n:::danger[セキュリティ警告]\n**プレーンテキストのパスワードをファイルに保存しないでください！** この JSON の例ではデモンストレーションのためにパスワードフィールドを表示していますが、以下の点に注意してください。\n\n- **SSH キーを優先** (`keyId`) し、`pwd` の使用は避けてください。その方が安全です。\n- パスワードを使用する必要がある場合は、**シークレットマネージャー**や環境変数を使用してください。\n- インポート後は**直ちにファイルを削除**してください。資格情報を放置しないでください。\n- **.gitignore に追加**してください。資格情報ファイルをバージョン管理にコミットしないでください。\n:::\n\n```json\n[\n  {\n    \"name\": \"My Server\",\n    \"ip\": \"example.com\",\n    \"port\": 22,\n    \"user\": \"root\",\n    \"pwd\": \"password\",\n    \"keyId\": \"\",\n    \"tags\": [\"production\"],\n    \"autoConnect\": false\n  }\n]\n```\n\n## フィールド\n\n| フィールド | 必須 | 説明 |\n|-------|----------|-------------|\n| `name` | はい | 表示名 |\n| `ip` | はい | ドメインまたは IP アドレス |\n| `port` | はい | SSH ポート (通常は 22) |\n| `user` | はい | SSH ユーザー名 |\n| `pwd` | いいえ | パスワード (非推奨 - 代わりに SSH キーを使用してください) |\n| `keyId` | いいえ | SSH キー名 (「非公開鍵」から取得 - 推奨) |\n| `tags` | いいえ | 整理用タグ |\n| `autoConnect` | いいえ | 起動時に自動接続 |\n\n## インポートの手順\n\n1. サーバー設定を含む JSON ファイルを作成する\n2. 設定 → バックアップ → サーバーの一括インポート\n3. JSON ファイルを選択する\n4. インポートを確認する\n\n## 例\n\n```json\n[\n  {\n    \"name\": \"Production\",\n    \"ip\": \"prod.example.com\",\n    \"port\": 22,\n    \"user\": \"admin\",\n    \"keyId\": \"my-key\",\n    \"tags\": [\"production\", \"web\"]\n  },\n  {\n    \"name\": \"Development\",\n    \"ip\": \"dev.example.com\",\n    \"port\": 2222,\n    \"user\": \"dev\",\n    \"keyId\": \"dev-key\",\n    \"tags\": [\"development\"]\n  }\n]\n```\n\n## ヒント\n\n- 可能な限りパスワードの代わりに **SSH キーを使用**してください。\n- インポート後に**接続をテスト**してください。\n- 管理を容易にするために**タグで整理**してください。\n- インポート後に **JSON ファイルを削除**してください。\n- 資格情報を含む JSON ファイルを**決してコミットしないでください**。\n"
  },
  {
    "path": "docs/src/content/docs/ja/advanced/custom-commands.md",
    "content": "---\ntitle: カスタムコマンド\ndescription: サーバーページにカスタムコマンドの出力を表示する\n---\n\nカスタムシェルコマンドを追加して、サーバー詳細ページに出力を表示します。\n\n## 設定\n\n1. サーバー設定 → カスタムコマンド\n2. JSON 形式でコマンドを入力\n\n## 基本形式\n\n```json\n{\n  \"表示名\": \"シェルコマンド\"\n}\n```\n\n**例:**\n```json\n{\n  \"メモリ\": \"free -h\",\n  \"ディスク\": \"df -h\",\n  \"稼働時間\": \"uptime\"\n}\n```\n\n## 結果の確認\n\n設定後、カスタムコマンドがサーバー詳細ページに表示され、自動的に更新されます。\n\n## 特殊なコマンド名\n\n### server_card_top_right\n\nホーム画面のサーバーカード（右上の隅）に表示されます。\n\n```json\n{\n  \"server_card_top_right\": \"実行したいコマンド\"\n}\n```\n\n## ヒント\n\n**絶対パスを使用する:**\n```json\n{\"マイ・スクリプト\": \"/usr/local/bin/my-script.sh\"}\n```\n\n**パイプを使用する:**\n```json\n{\"トッププロセス\": \"ps aux | sort -rk 3 | head -5\"}\n```\n\n**出力を整形する:**\n```json\n{\"CPU負荷\": \"uptime | awk -F'load average:' '{print $2}'\"}\n```\n\n**コマンドを高速に保つ:** 最高の体験のために 5 秒以内に完了するようにしてください。\n\n**出力を制限する:**\n```json\n{\"ログ\": \"tail -20 /var/log/syslog\"}\n```\n\n## セキュリティ\n\nコマンドは SSH ユーザーの権限で実行されます。システムの整合性を変更するようなコマンドは避けてください。\n"
  },
  {
    "path": "docs/src/content/docs/ja/advanced/custom-logo.md",
    "content": "---\ntitle: カスタムサーバーロゴ\ndescription: サーバーカードにカスタム画像を使用する\n---\n\n画像の URL を使用して、サーバーカードにカスタムロゴを表示します。\n\n## 設定\n\n1. サーバー設定 → カスタムロゴ\n2. 画像の URL を入力\n\n## URL プレースホルダー\n\n### {DIST} - Linux ディストリビューション\n\n検出されたディストリビューションに自動的に置換されます。\n\n```\nhttps://example.com/{DIST}.png\n```\n\n例: `debian.png`、`ubuntu.png`、`arch.png` など。\n\n### {BRIGHT} - テーマ\n\n現在のテーマに自動的に置換されます。\n\n```\nhttps://example.com/{BRIGHT}.png\n```\n\n例: `light.png` または `dark.png`。\n\n### 両方を組み合わせる\n\n```\nhttps://example.com/{DIST}-{BRIGHT}.png\n```\n\n例: `debian-light.png`、`ubuntu-dark.png` など。\n\n## ヒント\n\n- PNG または SVG 形式を使用してください。\n- 推奨サイズ: 64x64 〜 128x128 ピクセル。\n- HTTPS の URL を使用してください。\n- ファイルサイズは小さく保ってください。\n\n## サポートされているディストリビューション\n\ndebian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt\n\n全リスト: [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)\n"
  },
  {
    "path": "docs/src/content/docs/ja/advanced/json-settings.md",
    "content": "---\ntitle: 隠し設定 (JSON)\ndescription: JSON エディタ経由で詳細設定にアクセスする\n---\n\n一部の設定は UI 上では非表示になっていますが、JSON エディタからアクセス可能です。\n\n## アクセス方法\n\nサイドメニューの**「設定」**を長押しすると、JSON エディタが開きます。\n\n## よく使われる隠し設定\n\n### timeOut\n\n接続のタイムアウト時間（秒）。\n\n```json\n{\"timeOut\": 10}\n```\n\n**型:** integer | **デフォルト:** 5 | **範囲:** 1-60\n\n### recordHistory\n\n履歴（SFTP パスなど）を保存します。\n\n```json\n{\"recordHistory\": true}\n```\n\n**型:** boolean | **デフォルト:** true\n\n### textFactor\n\nテキストの倍率。\n\n```json\n{\"textFactor\": 1.2}\n```\n\n**型:** double | **デフォルト:** 1.0 | **範囲:** 0.8-1.5\n\n## その他の設定を探す\n\nすべての設定は [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart) で定義されています。\n\n以下の形式を探してください。\n```dart\nlate final settingName = StoreProperty(box, 'settingKey', defaultValue);\n```\n\n## ⚠️ 重要\n\n**編集する前に:**\n- **バックアップを作成する** - 設定を間違えるとアプリが起動しなくなる可能性があります。\n- **慎重に編集する** - JSON は有効な形式である必要があります。\n\n## 復旧方法\n\n編集後にアプリが起動しなくなった場合:\n1. アプリのデータを消去する（最終手段）\n2. アプリを再インストールする\n3. バックアップから復元する\n"
  },
  {
    "path": "docs/src/content/docs/ja/advanced/troubleshooting.md",
    "content": "---\ntitle: 一般的な問題\ndescription: よくある問題の解決策\n---\n\n## 接続の問題\n\n### SSH が接続できない\n\n**症状:** タイムアウト、接続拒否、認証失敗\n\n**解決策:**\n\n1. **サーバーの種類を確認する:** Unix 系のシステムのみがサポートされています (Linux, macOS, Android/Termux)。\n2. **手動でテストする:** `ssh user@server -p port`\n3. **ファイアウォールを確認する:** ポート 22 が開放されている必要があります。\n4. **資格情報を確認する:** ユーザー名とパスワード/キーが正しいことを確認してください。\n\n### 頻繁に切断される\n\n**症状:** 放置しているとターミナルが切断される\n\n**解決策:**\n\n1. **サーバーの Keep-alive 設定:**\n   ```bash\n   # /etc/ssh/sshd_config\n   ClientAliveInterval 60\n   ClientAliveCountMax 3\n   ```\n\n2. **バッテリーの最適化を無効にする:**\n   - MIUI: バッテリー → 「制限なし」\n   - Android: 設定 → アプリ → 最適化を無効化\n   - iOS: バックグラウンド更新を有効にする\n\n## 入力の問題\n\n### 特定の文字が入力できない\n\n**解決策:** 設定 → キーボードの種類 → `visiblePassword` に切り替える\n\n注意: この変更後、CJK（中日韓）入力が動作しなくなる可能性があります。\n\n## アプリの問題\n\n### 起動時にアプリがクラッシュする\n\n**症状:** アプリが開かない、画面が真っ暗になる\n\n**原因:** 設定の破損（特に JSON エディタによる編集後）\n\n**解決策:**\n\n1. **アプリのデータを消去する:**\n   - Android: 設定 → アプリ → ServerBox → データを消去\n   - iOS: アプリを削除して再インストール\n\n2. **バックアップから復元する:** 設定を変更する前に作成したバックアップをインポートしてください。\n\n### バックアップ/復元の問題\n\n**バックアップが動作しない:**\n- ストレージの空き容量を確認してください。\n- アプリにストレージ権限があるか確認してください。\n- 別の保存場所を試してください。\n\n**復元に失敗する:**\n- バックアップファイルの整合性を確認してください。\n- アプリのバージョンの互換性を確認してください。\n\n## ウィジェットの問題\n\n### ウィジェットが更新されない\n\n**iOS:**\n- 自動更新まで最大 30 分待機してください。\n- ウィジェットを一度削除してから再度追加してください。\n- URL が `/status` で終わっているか確認してください。\n\n**Android:**\n- ウィジェットをタップして強制的に更新してください。\n- ウィジェット ID がアプリ設定の構成と一致しているか確認してください。\n\n**watchOS:**\n- ウォッチアプリを再起動してください。\n- 設定変更後、数分待機してください。\n- URL の形式を確認してください。\n\n### ウィジェットにエラーが表示される\n\n- サーバーで ServerBox Monitor が実行されているか確認してください。\n- ブラウザで URL をテストしてください。\n- 認証資格情報を確認してください。\n\n## パフォーマンスの問題\n\n### アプリが重い\n\n**解決策:**\n- 設定で更新頻度を下げてください。\n- ネットワーク速度を確認してください。\n- 使用していないサーバーを無効にしてください。\n\n### バッテリー消費が激しい\n\n**解決策:**\n- 更新間隔を長くしてください。\n- バックグラウンド更新を無効にしてください。\n- 使用していない SSH セッションを閉じてください。\n\n## サポートを受ける\n\n問題が解決しない場合:\n\n1. **GitHub Issues を検索する:** https://github.com/lollipopkit/flutter_server_box/issues\n2. **新しい Issue を作成する:** アプリのバージョン、プラットフォーム、再現手順を含めてください。\n3. **Wiki を確認する:** 本ドキュメントおよび GitHub Wiki を参照してください。\n"
  },
  {
    "path": "docs/src/content/docs/ja/advanced/widgets.md",
    "content": "---\ntitle: ホーム画面ウィジェット\ndescription: サーバーの状態を表示するウィジェットをホーム画面に追加する\n---\n\nサーバーに [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) がインストールされている必要があります。\n\n## 前提条件\n\nまずサーバーに ServerBox Monitor をインストールしてください。設定手順については [ServerBox Monitor Wiki](https://github.com/lollipopkit/server_box_monitor/wiki/Home) を参照してください。\n\nインストール後、サーバーで以下が利用可能である必要があります。\n- HTTP/HTTPS エンドポイント\n- `/status` API エンドポイント\n- オプションの認証\n\n## URL 形式\n\n```\nhttps://your-server.com/status\n```\n\n末尾は必ず `/status` である必要があります。\n\n## iOS ウィジェット\n\n### 設定\n\n1. ホーム画面を長押し → **+** をタップ\n2. 「ServerBox」を検索\n3. ウィジェットのサイズを選択\n4. ウィジェットを長押し → **ウィジェットを編集**\n5. `/status` で終わる URL を入力\n\n### 注意事項\n\n- HTTPS を使用する必要があります（ローカル IP を除く）。\n- 最大更新頻度: 30 分（iOS の制限）。\n- 複数のサーバーに対して複数のウィジェットを追加できます。\n\n## Android ウィジェット\n\n### 設定\n\n1. ホーム画面を長押し → **ウィジェット**\n2. 「ServerBox」を見つける → ホーム画面に追加\n3. 表示されたウィジェット ID 番号をメモする\n4. ServerBox アプリを開く → 設定\n5. **「ホームウィジェットのリンクを構成」**をタップ\n6. エントリを追加: `ウィジェット ID` = `ステータス URL`\n\n例:\n- キー: `17`\n- 値: `https://my-server.com/status`\n\n7. ホーム画面のウィジェットをタップして更新\n\n## watchOS ウィジェット\n\n### 設定\n\n1. iPhone アプリを開く → 設定\n2. **iOS 設定** → **Watch アプリ**\n3. **「URL を追加」**をタップ\n4. `/status` で終わる URL を入力\n5. ウォッチアプリが同期されるのを待つ\n\n### 注意事項\n\n- 更新されない場合は、ウォッチアプリの再起動を試してください。\n- iPhone と Apple Watch が接続されていることを確認してください。\n\n## トラブルシューティング\n\n### ウィジェットが更新されない\n\n**iOS:** 最大 30 分待機し、その後ウィジェットを削除して再追加してください。\n**Android:** ウィジェットをタップして強制的に更新し、設定で ID を確認してください。\n**watchOS:** ウォッチアプリを再起動し、数分待機してください。\n\n### ウィジェットにエラーが表示される\n\n- ServerBox Monitor が実行されているか確認してください。\n- ブラウザで URL をテストしてください。\n- URL が `/status` で終わっているか確認してください。\n\n## セキュリティ\n\n- 可能な限り**常に HTTPS を使用**してください。\n- **ローカル IP** は信頼できるネットワーク内でのみ使用してください。\n"
  },
  {
    "path": "docs/src/content/docs/ja/development/architecture.md",
    "content": "---\ntitle: アーキテクチャ\ndescription: アーキテクチャパターンと設計上の決定事項\n---\n\nServer Box は、データ層、ドメイン層、プレゼンテーション層を明確に分離したクリーンアーキテクチャの原則に従っています。\n\n## 階層型アーキテクチャ\n\n```\n┌─────────────────────────────────────┐\n│          プレゼンテーション層       │\n│         (lib/view/page/)            │\n│  - ページ、ウィジェット、コントローラー  │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│         ビジネスロジック層          │\n│      (lib/data/provider/)           │\n│  - Riverpod Provider                │\n│  - 状態管理                         │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│           データ層                  │\n│      (lib/data/model/, store/)      │\n│  - モデル、ストレージ、サービス      │\n└─────────────────────────────────────┘\n```\n\n## 主要なパターン\n\n### 状態管理: Riverpod\n\n- **コード生成**: 型安全な Provider のために `riverpod_generator` を使用\n- **State Notifier**: ビジネスロジックを伴う可変状態に使用\n- **Async Notifier**: ロード中やエラー状態の管理に使用\n- **Stream Provider**: リアルタイムデータに使用\n\n### 不変モデル: Freezed\n\n- すべてのデータモデルで Freezed による不変性を確保\n- 状態表現のための Union 型の活用\n- 組み込みの JSON シリアライズ\n- 更新のための CopyWith エクステンション\n\n### ローカルストレージ: Hive\n\n- **hive_ce**: Hive のコミュニティ版を使用\n- 手動での `@HiveField` や `@HiveType` の指定が不要\n- 型アダプターの自動生成\n- 永続的なキーバリューストレージ\n\n## 依存関係の注入 (DI)\n\nサービスやストアは以下を通じて注入されます。\n\n1. **Provider**: UI に対して依存関係を公開\n2. **GetIt**: サービスロケーター（必要に応じて使用）\n3. **コンストラクタ注入**: 明示的な依存関係の定義\n\n## データフロー\n\n```\nユーザーアクション → Widget → Provider → サービス/ストア → モデル更新 → UI 再構築\n```\n\n1. ユーザーがウィジェットを操作\n2. ウィジェットが Provider のメソッドを呼び出す\n3. Provider がサービス/ストアを通じて状態を更新\n4. 状態の変化により UI の再構築がトリガーされる\n5. 新しい状態がウィジェットに反映される\n\n## カスタム依存関係\n\nプロジェクトでは、機能を拡張するためにいくつかのカスタムフォークを使用しています。\n\n- **dartssh2**: 強化された SSH 機能\n- **xterm**: モバイル対応のターミナルエミュレータ\n- **fl_lib**: 共有 UI コンポーネントとユーティリティ\n\n## スレッディング\n\n- **Isolate**: 重い計算処理をメインスレッドから分離\n- **computer パッケージ**: マルチスレッド用のユーティリティ\n- **Async/Await**: 非ブロッキングな I/O 操作\n"
  },
  {
    "path": "docs/src/content/docs/ja/development/building.md",
    "content": "---\ntitle: ビルド\ndescription: 各プラットフォーム向けのビルド手順\n---\n\nServer Box は、クロスプラットフォームビルドのためにカスタムビルドシステム (`fl_build`) を使用しています。\n\n## 前提条件\n\n- Flutter SDK (stable チャネル)\n- プラットフォーム固有のツール (iOS 用の Xcode、Android 用の Android Studio)\n- Rust ツールチェーン (一部のネイティブ依存関係のため)\n\n## 開発用ビルド\n\n```bash\n# 開発モードで実行\nflutter run\n\n# 特定のデバイスで実行\nflutter run -d <device-id>\n```\n\n## 製品用ビルド\n\nプロジェクトではビルドに `fl_build` を使用します。\n\n```bash\n# 特定のプラットフォーム向けにビルド\ndart run fl_build -p <platform>\n\n# 利用可能なプラットフォーム:\n# - ios\n# - android\n# - macos\n# - linux\n# - windows\n```\n\n## プラットフォーム固有のビルド\n\n### iOS\n\n```bash\ndart run fl_build -p ios\n```\n\n以下が必要です。\n- Xcode がインストールされた macOS\n- CocoaPods\n- 署名用の Apple Developer アカウント\n\n### Android\n\n```bash\ndart run fl_build -p android\n```\n\n以下が必要です。\n- Android SDK\n- Java Development Kit (JDK)\n- 署名用のキーストア\n\n### macOS\n\n```bash\ndart run fl_build -p macos\n```\n\n### Linux\n\n```bash\ndart run fl_build -p linux\n```\n\n### Windows\n\n```bash\ndart run fl_build -p windows\n```\n\nVisual Studio がインストールされた Windows が必要です。\n\n## ビルド前/後処理\n\n`make.dart` スクリプトが以下を処理します。\n\n- メタデータの生成\n- バージョン文字列の更新\n- プラットフォーム固有の構成\n\n## トラブルシューティング\n\n### クリーンビルド\n\n```bash\nflutter clean\ndart run build_runner build --delete-conflicting-outputs\nflutter pub get\n```\n\n### バージョンの不一致\n\nすべての依存関係に互換性があることを確認してください。\n```bash\nflutter pub upgrade\n```\n\n## リリースチェックリスト\n\n1. `pubspec.yaml` のバージョンを更新する\n2. コード生成を実行する\n3. テストを実行する\n4. すべてのターゲットプラットフォーム向けにビルドする\n5. 実機でテストする\n6. GitHub リリースを作成する\n"
  },
  {
    "path": "docs/src/content/docs/ja/development/codegen.md",
    "content": "---\ntitle: コード生成\ndescription: build_runner を使用したコード生成\n---\n\nServer Box では、モデル、状態管理、シリアライズのためにコード生成を多用しています。\n\n## コード生成を実行するタイミング\n\n以下のファイルを変更した後に実行してください。\n\n- `@freezed` アノテーションが付いたモデル\n- `@JsonSerializable` が付いたクラス\n- Hive モデル\n- `@riverpod` が付いた Provider\n- ローカライズファイル (ARB ファイル)\n\n## コード生成の実行\n\n```bash\n# すべてのコードを生成\ndart run build_runner build --delete-conflicting-outputs\n\n# クリーンアップして再生成\ndart run build_runner build --delete-conflicting-outputs --clean\n```\n\n## 生成されるファイル\n\n### Freezed (`*.freezed.dart`)\n\nUnion 型を持つ不変データモデル:\n\n```dart\n@freezed\nclass ServerState with _$ServerState {\n  const factory ServerState.connected() = Connected;\n  const factory ServerState.disconnected() = Disconnected;\n  const factory ServerState.error(String message) = Error;\n}\n```\n\n### JSON シリアライズ (`*.g.dart`)\n\n`json_serializable` によって生成されます:\n\n```dart\n@JsonSerializable()\nclass Server {\n  final String id;\n  final String name;\n  final String host;\n\n  Server({required this.id, required this.name, required this.host});\n\n  factory Server.fromJson(Map<String, dynamic> json) =>\n      _$ServerFromJson(json);\n  Map<String, dynamic> toJson() => _$ServerToJson(this);\n}\n```\n\n### Riverpod Provider (`*.g.dart`)\n\n`@riverpod` アノテーションから生成されます:\n\n```dart\n@riverpod\nclass MyNotifier extends _$MyNotifier {\n  @override\n  int build() => 0;\n}\n```\n\n### Hive アダプター (`*.g.dart`)\n\nHive モデル (hive_ce) 用に自動生成されます:\n\n```dart\n@HiveType(typeId: 0)\nclass ServerModel {\n  @HiveField(0)\n  final String id;\n}\n```\n\n## ローカライズの生成\n\n```bash\nflutter gen-l10n\n```\n\n`lib/l10n/*.arb` ファイルから `lib/generated/l10n/` を生成します。\n\n## ヒント\n\n- 競合を避けるために `--delete-conflicting-outputs` を使用してください。\n- 生成されたファイルを `.gitignore` に追加してください。\n- 生成されたファイルを手動で編集しないでください。\n"
  },
  {
    "path": "docs/src/content/docs/ja/development/state.md",
    "content": "---\ntitle: 状態管理\ndescription: Riverpod ベースの状態管理パターン\n---\n\nServer Box は、状態管理のためにコード生成を伴う Riverpod を使用しています。\n\n## Provider の種類\n\n### StateProvider\n\n読み書き可能な単純な状態:\n\n```dart\n@riverpod\nclass Settings extends _$Settings {\n  @override\n  SettingsModel build() {\n    return SettingsModel.defaults();\n  }\n\n  void update(SettingsModel newSettings) {\n    state = newSettings;\n  }\n}\n```\n\n### AsyncNotifierProvider\n\nロード中やエラー状態を伴う、非同期にロードされる状態:\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    return fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() => fetchStatus(server));\n  }\n}\n```\n\n### StreamProvider\n\nストリームからのリアルタイムデータ:\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  return cpuService.monitor(server);\n}\n```\n\n## 状態パターン\n\n### ロード状態\n\n```dart\nstate.when(\n  data: (data) => DataWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### Family Provider\n\nパラメータを受け取る Provider:\n\n```dart\n@riverpod\nList<Container> containers(ContainersRef ref, Server server) {\n  return containerService.list(server);\n}\n```\n\n### Auto-Dispose\n\n参照されなくなったときに破棄される Provider:\n\n```dart\n@Riverpod(keepAlive: false)\nclass TempState extends _$TempState {\n  // ...\n}\n```\n\n## ベストプラクティス\n\n1. **コード生成を使用する**: 常に `@riverpod` アノテーションを使用してください\n2. **Provider を近くに配置する**: 利用するウィジェットの近くに定義してください\n3. **シングルトンを避ける**: 代わりに Provider を使用してください\n4. **適切に階層化する**: UI ロジックとビジネスロジックを分離してください\n\n## ウィジェットでの状態の読み取り\n\n```dart\nclass ServerWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final status = ref.watch(serverStatusProvider(server));\n    return status.when(...);\n  }\n}\n```\n\n## 状態の変更\n\n```dart\nref.read(settingsProvider.notifier).update(newSettings);\n```\n"
  },
  {
    "path": "docs/src/content/docs/ja/development/structure.md",
    "content": "---\ntitle: プロジェクト構造\ndescription: Server Box のコードベースを理解する\n---\n\nServer Box プロジェクトは、関心の分離を明確にしたモジュール式のアーキテクチャを採用しています。\n\n## ディレクトリ構造\n\n```\nlib/\n├── core/              # コアユーティリティとエクステンション\n├── data/              # データ層\n│   ├── model/         # 機能別のデータモデル\n│   ├── provider/      # Riverpod Provider\n│   └── store/         # ローカルストレージ (Hive)\n├── view/              # UI 層\n│   ├── page/          # 主要なページ\n│   └── widget/        # 再利用可能なウィジェット\n├── generated/         # 生成されたローカライズファイル\n├── l10n/              # ローカライズ用 ARB ファイル\n└── hive/              # Hive アダプター\n```\n\n## コア層 (`lib/core/`)\n\nユーティリティ、エクステンション、およびルーティング構成が含まれます。\n\n- **Extensions**: 一般的な型に対する Dart のエクステンション\n- **Routes**: アプリのルーティング構成\n- **Utils**: 共有ユーティリティ関数\n\n## データ層 (`lib/data/`)\n\n### モデル (`lib/data/model/`)\n\n機能ごとに整理されています。\n\n- `server/` - サーバー接続およびステータスモデル\n- `container/` - Docker コンテナモデル\n- `ssh/` - SSH セッションモデル\n- `sftp/` - SFTP ファイルモデル\n- `app/` - アプリ固有のモデル\n\n### Provider (`lib/data/provider/`)\n\n依存関係の注入と状態管理のための Riverpod Provider。\n\n- サーバー関連の Provider\n- UI 状態の Provider\n- サービス関連の Provider\n\n### ストア (`lib/data/store/`)\n\nHive ベースのローカルストレージ。\n\n- サーバー情報の保存\n- 設定情報の保存\n- キャッシュ情報の保存\n\n## UI 層 (`lib/view/`)\n\n### ページ (`lib/view/page/`)\n\nアプリケーションのメイン画面。\n\n- `server/` - サーバー管理ページ\n- `ssh/` - SSH ターミナルページ\n- `container/` - コンテナ管理ページ\n- `setting/` - 設定ページ\n- `storage/` - SFTP ページ\n- `snippet/` - スニペットページ\n\n### ウィジェット (`lib/view/widget/`)\n\n再利用可能な UI コンポーネント。\n\n- サーバーカード\n- ステータスチャート\n- 入力コンポーネント\n- ダイアログ\n\n## 生成されたファイル\n\n- `lib/generated/l10n/` - 自動生成されたローカライズコード\n- `*.g.dart` - 生成されたコード (json_serializable, freezed, hive, riverpod)\n- `*.freezed.dart` - Freezed による不変クラス\n\n## パッケージディレクトリ (`/packages/`)\n\n依存関係のカスタムフォークが含まれています。\n\n- `dartssh2/` - SSH ライブラリ\n- `xterm/` - ターミナルエミュレータ\n- `fl_lib/` - 共有ユーティリティ\n- `fl_build/` - ビルドシステム\n"
  },
  {
    "path": "docs/src/content/docs/ja/development/testing.md",
    "content": "---\ntitle: テスト\ndescription: テスト戦略とテストの実行\n---\n\n## テストの実行\n\n```bash\n# すべてのテストを実行\nflutter test\n\n# 特定のテストファイルを実行\nflutter test test/battery_test.dart\n\n# カバレッジ付きで実行\nflutter test --coverage\n```\n\n## テスト構造\n\nテストは `test/` ディレクトリにあり、lib の構造を反映しています：\n\n```\ntest/\n├── data/\n│   ├── model/\n│   └── provider/\n├── view/\n│   └── widget/\n└── test_helpers.dart\n```\n\n## ユニットテスト\n\nビジネスロジックとデータモデルのテスト：\n\n```dart\ntest('CPU使用率を計算する必要があります', () {\n  final cpu = CpuModel(usage: 75.0);\n  expect(cpu.usagePercentage, '75%');\n});\n```\n\n## ウィジェットテスト\n\nUI コンポーネントのテスト：\n\n```dart\ntestWidgets('ServerCard にサーバー名が表示されること', (tester) async {\n  await tester.pumpWidget(\n    ProviderScope(\n      child: MaterialApp(\n        home: ServerCard(server: testServer),\n      ),\n    ),\n  );\n\n  expect(find.text('Test Server'), findsOneWidget);\n});\n```\n\n## プロバイダーテスト\n\nRiverpod プロバイダーのテスト：\n\n```dart\ntest('serverStatusProvider がステータスを返すこと', () async {\n  final container = ProviderContainer();\n  final status = await container.read(serverStatusProvider(testServer).future);\n  expect(status, isA<StatusModel>());\n});\n```\n\n## モック (Mocking)\n\n外部依存関係にモックを使用する：\n\n```dart\nclass MockSshService extends Mock implements SshService {}\n\ntest('サーバーに接続すること', () async {\n  final mockSsh = MockSshService();\n  when(mockSsh.connect(any)).thenAnswer((_) async => true);\n\n  // モックを使用してテスト\n});\n```\n\n## 統合テスト\n\n完全なユーザーフローのテスト (`integration_test/` 内)：\n\n```dart\ntestWidgets('サーバー追加フロー', (tester) async {\n  await tester.pumpWidget(MyApp());\n\n  // 追加ボタンをタップ\n  await tester.tap(find.byIcon(Icons.add));\n  await tester.pumpAndSettle();\n\n  // フォームを入力\n  await tester.enterText(find.byKey(Key('name')), 'Test Server');\n  // ...\n});\n```\n\n## ベストプラクティス\n\n1. **Arrange-Act-Assert**: テストを明確に構造化する\n2. **記述的な名前**: テスト名は動作を記述する必要がある\n3. **1つのテストに1つのアサーション**: テストの焦点を絞る\n4. **外部依存関係をモックする**: 実際のサーバーに依存しない\n5. **エッジケースのテスト**: 空のリスト、null 値など\n"
  },
  {
    "path": "docs/src/content/docs/ja/index.mdx",
    "content": "---\ntitle: Server Box\ndescription: 総合的なクロスプラットフォーム・サーバー管理アプリケーション\nhero:\n  tagline: どこからでも Linux サーバーを管理\n  actions:\n    - text: はじめる\n      link: /ja/introduction/\n      icon: right-arrow\n      variant: primary\n    - text: GitHub で見る\n      link: https://github.com/lollipopkit/flutter_server_box\n      icon: github\n      variant: minimal\n---\n\nimport { Card, CardGrid } from '@astrojs/starlight/components';\n\n## 特徴\n\n<CardGrid stagger>\n\t<Card title=\"リアルタイム監視\" icon=\"chart\">\n\t\tCPU、メモリ、ディスク、ネットワーク、GPU、温度を美しいリアルタイムチャートで監視します。\n\t</Card>\n\t<Card title=\"SSH ターミナル\" icon=\"terminal\">\n\t\tマルチタブ対応、モバイルデバイス用仮想キーボードを備えたフル機能の SSH ターミナル。\n\t</Card>\n\t<Card title=\"SFTP ファイルブラウザ\" icon=\"folder\">\n\t\t内蔵の SFTP クライアントとローカルファイルブラウザでサーバー上のファイルを管理。\n\t</Card>\n\t<Card title=\"Docker 管理\" icon=\"box\">\n\t\t直感的なインターフェースで Docker コンテナの起動、停止、監視が可能。\n\t</Card>\n\t<Card title=\"クロスプラットフォーム\" icon=\"device-mobile\">\n\t\tiOS、Android、macOS、Linux、Windows、watchOS で利用可能。\n\t</Card>\n\t<Card title=\"12以上の言語\" icon=\"globe\">\n\t\t英語、中国語、ドイツ語、フランス語など、フルローカライズをサポート。\n\t</Card>\n</CardGrid>\n\n## クイックリンク\n\n- **ダウンロード**: [App Store](https://apps.apple.com/app/id1586449703)、[GitHub](https://github.com/lollipopkit/flutter_server_box/releases)、[F-Droid](https://f-droid.org/) で入手可能\n- **ドキュメント**: Server Box を使い始めるためのガイドを確認\n- **サポート**: GitHub コミュニティに参加して議論や問題報告を行う\n"
  },
  {
    "path": "docs/src/content/docs/ja/installation.mdx",
    "content": "---\ntitle: インストール\ndescription: お使いのデバイスに Server Box をダウンロードしてインストールする\n---\n\nServer Box は複数のプラットフォームで利用可能です。お好みのインストール方法を選択してください。\n\n## モバイルアプリ\n\n### iOS\n\n**[App Store](https://apps.apple.com/app/id1586449703)** からダウンロードしてください。\n\n### Android\n\nお好みのソースを選択してください：\n\n- **[F-Droid](https://f-droid.org/)** - FOSS（自由でオープンソースのソフトウェア）のみのソースを好むユーザー向け\n- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** - ソースから直接最新バージョンを入手したいユーザー向け\n\n## デスクトップアプリ\n\n### macOS\n\n**[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** からダウンロードしてください。\n\n特徴：\n- ネイティブメニューバーへの統合\n- Intel と Apple Silicon の両方をサポート\n\n### Linux\n\n**[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** からダウンロードしてください。\n\nAppImage、deb、または tar.gz パッケージとして利用可能です。\n\n### Windows\n\n**[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** からダウンロードしてください。\n\n## watchOS\n\niOS アプリの一部として **[App Store](https://apps.apple.com/app/id1586449703)** で利用可能です。\n\n## ソースからのビルド\n\nServer Box をソースからビルドするには、開発ドキュメントの [ビルド](/ja/development/building/) セクションを参照してください。\n\n## バージョン情報\n\n最新バージョンと変更履歴については、[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) ページを確認してください。\n"
  },
  {
    "path": "docs/src/content/docs/ja/introduction.mdx",
    "content": "---\ntitle: はじめに\ndescription: Server Box とはその何か、何ができるのかを学ぶ\n---\n\nServer Box は、Flutter で構築された包括的なクロスプラットフォームのサーバー管理アプリケーションです。Linux、Unix、および Windows サーバーをどこからでも監視、管理、および制御できます。\n\n## Server Box とは？\n\nServer Box は、SSH 接続を介してサーバー管理タスクのための統一されたインターフェースを提供します。システム管理者、開発者、またはホームサーバーを運用している愛好家であっても、このアプリは強力なサーバー管理ツールをあなたのポケットに届けます。\n\n## 主な機能\n\n- **リアルタイム監視**: CPU、メモリ、ディスク使用量、ネットワーク速度、GPU ステータス、およびシステム温度を追跡\n- **SSH ターミナル**: マルチタブをサポートし、外観をカスタマイズ可能なフルターミナルアクセス\n- **SFTP クライアント**: サーバー上のファイルを閲覧および管理\n- **Docker 管理**: コンテナを簡単に制御\n- **プロセス管理**: システムプロセスを表示および管理\n- **Systemd サービス**: systemd サービスの開始、停止、および監視\n- **ネットワークツール**: iPerf テスト、ping、および Wake-on-LAN\n- **スニペット**: カスタムシェルコマンドを保存して実行\n\n## サポートされているプラットフォーム\n\nServer Box は真のクロスプラットフォームです：\n\n- **モバイル**: iOS および Android\n- **デスクトップ**: macOS、Linux、および Windows\n\n## ライセンス\n\nこのプロジェクトは AGPL v3 ライセンスの下で提供されています。ソースコードは [GitHub](https://github.com/lollipopkit/flutter_server_box) で公開されています。\n"
  },
  {
    "path": "docs/src/content/docs/ja/platforms/desktop.md",
    "content": "---\ntitle: デスクトップ版の機能\ndescription: macOS、Linux、Windows 特有の機能\n---\n\nデスクトッププラットフォーム上の Server Box は、さらなる生産性向上機能を提供します。\n\n## macOS\n\n### メニューバーへの統合\n\n- メニューバーでサーバーの状態を素早く確認\n- ワンクリックでサーバーにアクセス\n- 集中を妨げないコンパクトモード\n- ネイティブな macOS メニューバースタイル\n\n### ウィンドウ状態の保持\n\n- ウィンドウの位置とサイズを記憶\n- 起動時に前回のセッションを復元\n- マルチモニター対応\n\n### ネイティブ機能\n\n- **タイトルバー**: カスタムまたはシステムタイトルバーの選択オプション\n- **フルスクリーンモード**: 専用のサーバー監視ビュー\n- **キーボードショートカット**: macOS ネイティブなショートカット\n- **Touch Bar**（対応デバイス）: クイックアクション\n\n## Linux\n\n### ネイティブ統合\n\n- システムトレイのサポート\n- デスクトップ通知の統合\n- ファイルピッカーの統合\n\n### ウィンドウ管理\n\n- X11 および Wayland のサポート\n- タイル型ウィンドウマネージャーに最適化\n- カスタムウィンドウ装飾オプション\n\n## Windows\n\n### 機能\n\n- システムトレイへの統合\n- ジャンプリストによるクイックアクション\n- ネイティブなウィンドウコントロール\n- 起動時の自動実行オプション\n\n## クロスプラットフォーム・デスクトップ機能\n\n### キーボードショートカット\n\n**macOS のみ:**\n- **Cmd + ,**: 設定\n- **Cmd + Q**: アプリを終了\n- **Cmd + 1-9**: タブ切り替え\n\n:::note\nWindows と Linux では、ネイティブなメニューバーとキーボードショートカットは現在実装されていません。\n:::\n\n### テーマ\n\n- ライトテーマ\n- ダークテーマ\n- AMOLED テーマ（純粋な黒）\n- システムテーマ（OS の設定に追従）\n\n### モバイル版に対する利点\n\n- 監視用の広い画面\n- ターミナル操作に最適なフルキーボード\n- 高速なファイル操作\n- 優れたマルチタスク性能\n"
  },
  {
    "path": "docs/src/content/docs/ja/platforms/mobile.md",
    "content": "---\ntitle: モバイル版の機能\ndescription: iOS および Android 特有の機能\n---\n\nServer Box は、iOS および Android デバイス向けにいくつかのモバイル特有の機能を提供しています。\n\n## 生体認証\n\n生体認証を使用してサーバーを保護します。\n\n- **iOS**: Face ID または Touch ID\n- **Android**: 指紋認証\n\n設定 > セキュリティ > 生体認証 から有効にできます。\n\n## ホーム画面ウィジェット\n\nホーム画面にサーバーの状態を表示するウィジェットを追加して、素早く監視できます。\n\n### iOS\n\n- ホーム画面を長押し\n- **+** をタップしてウィジェットを追加\n- 「Server Box」を検索\n- ウィジェットのサイズを選択:\n  - 小: 単一サーバーの状態\n  - 中: 複数サーバーの状態\n  - 大: 詳細情報\n\n### Android\n\n- ホーム画面を長押し\n- **ウィジェット**をタップ\n- 「Server Box」を見つける\n- ウィジェットの種類を選択\n\n## バックグラウンド実行\n\n### Android\n\nバックグラウンドで接続を維持します。\n\n- 設定 > 詳細設定 > バックグラウンド実行 から有効にできます。\n- バッテリーの最適化からの除外が必要です。\n- アクティブな接続については常駐通知が表示されます。\n\n### iOS\n\nバックグラウンド制限が適用されます。\n\n- バックグラウンドでは接続が一時停止する場合があります。\n- アプリに戻った際に素早く再接続されます。\n- バックグラウンド更新をサポートしています。\n\n## プッシュ通知\n\n以下の通知を受け取ることができます。\n\n- サーバーのオフライン通知\n- リソース使用率の高騰警告\n- タスク完了通知\n\n設定 > 通知 から構成できます。\n\n## モバイル UI 機能\n\n- **スワイプして更新**: サーバーの状態を更新\n- **スワイプアクション**: サーバーのクイック操作\n- **横画面モード**: ターミナルの操作性を向上\n- **仮想キーボード**: ターミナル用のショートカットキー\n\n## ファイルの統合\n\n- **ファイルアプリ (iOS)**: 「ファイル」アプリから SFTP に直接アクセス\n- **Storage Access Framework (Android)**: 他のアプリとファイルを共有\n- **ドキュメントピッカー**: 簡単なファイル選択\n"
  },
  {
    "path": "docs/src/content/docs/ja/principles/architecture.md",
    "content": "---\ntitle: アーキテクチャの概要\ndescription: アプリケーションのハイレベル・アーキテクチャ\n---\n\nServer Box は、関心の分離を明確にした階層型アーキテクチャを採用しています。\n\n## アーキテクチャ階層\n\n```\n┌─────────────────────────────────────────────────┐\n│          プレゼンテーション層 (UI)              │\n│          lib/view/page/, lib/view/widget/       │\n│  - ページ、ウィジェット、コントローラー          │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│         ビジネスロジック層                      │\n│         lib/data/provider/                      │\n│  - Riverpod Provider, State Notifier            │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│           データアクセス層                      │\n│         lib/data/store/, lib/data/model/        │\n│  - Hive Store, データモデル                     │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│         外部統合層                              │\n│  - SSH (dartssh2), ターミナル (xterm), SFTP     │\n│  - プラットフォーム固有コード (iOS, Android等)  │\n└─────────────────────────────────────────────────┘\n```\n\n## アプリケーションの基盤\n\n### メインエントリーポイント\n\n`lib/main.dart` がアプリを初期化します。\n\n```dart\nvoid main() {\n  runApp(\n    ProviderScope(\n      child: MyApp(),\n    ),\n  );\n}\n```\n\n### ルートウィジェット\n\n`MyApp` は以下を提供します。\n- **テーマ管理**: ライト/ダークテーマの切り替え\n- **ルーティング構成**: ナビゲーション構造\n- **Provider Scope**: 依存関係注入のルート\n\n### ホームページ\n\n`HomePage` はナビゲーションのハブとして機能します。\n- **タブインターフェース**: サーバー、スニペット、コンテナ、SSH\n- **状態管理**: タブごとの状態保持\n- **ナビゲーション**: 各機能へのアクセス\n\n## コアシステム\n\n### 状態管理: Riverpod\n\n**なぜ Riverpod なのか？**\n- コンパイル時の安全性\n- テストの容易さ\n- Build context への依存がない\n- プラットフォームを問わず動作する\n\n**使用されている Provider の種類:**\n- `StateProvider`: 単純な可変状態\n- `AsyncNotifierProvider`: ロード中/エラー/データ状態の管理\n- `StreamProvider`: リアルタイムデータストリーム\n- Future provider: 一回限りの非同期操作\n\n### データ永続化: Hive CE\n\n**なぜ Hive CE なのか？**\n- ネイティブコードへの依存がない\n- 高速なキーバリューストレージ\n- コード生成による型安全性\n- 手動でのフィールドアノテーションが不要\n\n**ストア:**\n- `SettingStore`: アプリの設定\n- `ServerStore`: サーバー構成\n- `SnippetStore`: コマンドスニペット\n- `KeyStore`: SSH キー\n\n### 不変モデル: Freezed\n\n**利点:**\n- コンパイル時の不変性\n- 状態管理のための Union 型\n- 組み込みの JSON シリアライズ\n- CopyWith エクステンション\n\n## クロスプラットフォーム戦略\n\n### プラグインシステム\n\nFlutter プラグインが各プラットフォームとの統合を提供します。\n\n| プラットフォーム | 統合方法 |\n|----------|-------------------|\n| iOS | CocoaPods, Swift/Obj-C |\n| Android | Gradle, Kotlin/Java |\n| macOS | CocoaPods, Swift |\n| Linux | CMake, C++ |\n| Windows | CMake, C# |\n\n### プラットフォーム固有機能\n\n**iOS のみ:**\n- ホーム画面ウィジェット\n- ライブアクティビティ\n- Apple Watch コンパニオン\n\n**Android のみ:**\n- バックグラウンドサービス\n- プッシュ通知\n- ファイルシステムへのアクセス\n\n**デスクトップのみ:**\n- メニューバーへの統合\n- マルチウィンドウ対応\n- カスタムタイトルバー\n\n## カスタム依存関係\n\n### dartssh2 (フォーク版)\n\n以下の機能を備えた強化版 SSH クライアント:\n- モバイルサポートの改善\n- エラーハンドリングの強化\n- パフォーマンスの最適化\n\n### xterm.dart (フォーク版)\n\n以下の機能を備えたターミナルエミュレータ:\n- モバイル向けに最適化されたレンダリング\n- タッチジェスチャーのサポート\n- 仮想キーボードとの統合\n\n### fl_lib\n\n以下の共有ユーティリティパッケージ:\n- 共通ウィジェット\n- エクステンション\n- ヘルパー関数\n\n## ビルドシステム\n\n### fl_build パッケージ\n\n以下のためのカスタムビルドシステム:\n- マルチプラットフォームビルド\n- コード署名\n- アセットのバンドル\n- バージョン管理\n\n### ビルドプロセス\n\n```\nmake.dart (バージョン計算) → fl_build (ビルド実行) → プラットフォーム別出力\n```\n\n1. **プリビルド**: Git からバージョンを算出\n2. **ビルド**: ターゲットプラットフォーム向けにコンパイル\n3. **ポストビルド**: パッケージングと署名\n\n## データフローの例\n\n### サーバー状態の更新\n\n```\n1. タイマーが発火 →\n2. Provider がサービスを呼び出す →\n3. サービスが SSH コマンドを実行 →\n4. レスポンスをモデルにパース →\n5. 状態が更新される →\n6. 新しいデータで UI が再構築される\n```\n\n### ユーザーアクションのフロー\n\n```\n1. ユーザーがボタンをタップ →\n2. ウィジェットが Provider のメソッドを呼び出す →\n3. Provider が状態を更新 →\n4. 状態の変化により再構築がトリガーされる →\n5. 新しい状態が UI に反映される\n```\n\n## セキュリティアーキテクチャ\n\n### データ保護\n\n- **パスワード**: flutter_secure_storage で暗号化\n- **SSH キー**: 保存時に暗号化\n- **ホストフィンガープリント**: 安全に保存\n- **セッションデータ**: 永続化しない\n\n### 接続のセキュリティ\n\n- **ホストキー検証**: 中間者攻撃の検知\n- **暗号化**: 標準的な SSH 暗号化\n- **平文保存の禁止**: 機密データは決して平文で保存しない\n"
  },
  {
    "path": "docs/src/content/docs/ja/principles/sftp.md",
    "content": "---\ntitle: SFTP システム\ndescription: SFTP ファイルブラウザの仕組み\n---\n\nSFTP システムは、SSH を介したファイル管理機能を提供します。\n\n## アーキテクチャ\n\n```\n┌─────────────────────────────────────────────┐\n│              SFTP UI レイヤー               │\n│  - ファイルブラウザ (リモート)              │\n│  - ファイルブラウザ (ローカル)              │\n│  - 転送キュー                               │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          SFTP 状態管理                      │\n│  - sftpProvider                             │\n│  - パス管理                                 │\n│  - 操作キュー                               │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         SFTP プロトコルレイヤー             │\n│  - SSH サブシステム                         │\n│  - ファイル操作                             │\n│  - ディレクトリ一覧取得                     │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│            SSH 転送                         │\n│  - セキュアチャネル                         │\n│  - データストリーミング                     │\n└─────────────────────────────────────────────┘\n```\n\n## 接続の確立\n\n### SFTP クライアントの作成\n\n```dart\nFuture<SftpClient> createSftpClient(Spi spi) async {\n  // 1. SSH クライアントを取得 (利用可能な場合は再利用)\n  final sshClient = await genClient(spi);\n\n  // 2. SFTP サブシステムを開く\n  final sftp = await sshClient.openSftp();\n\n  return sftp;\n}\n```\n\n### 接続の再利用\n\nSFTP は既存の SSH 接続を再利用します。\n\n```dart\nclass ServerProvider {\n  SSHClient? _sshClient;\n  SftpClient? _sftpClient;\n\n  Future<SftpClient> getSftpClient(String spiId) async {\n    _sftpClient ??= await _sshClient!.openSftp();\n    return _sftpClient!;\n  }\n}\n```\n\n## ファイルシステム操作\n\n### ディレクトリ一覧取得\n\n```dart\nFuture<List<SftpFile>> listDirectory(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // ディレクトリ一覧を取得\n  final files = await sftp.listDir(path);\n\n  // 設定に基づいてソート\n  files.sort((a, b) {\n    switch (sortOption) {\n      case SortOption.name:\n        return a.name.toLowerCase().compareTo(b.name.toLowerCase());\n      case SortOption.size:\n        return a.size.compareTo(b.size);\n      case SortOption.time:\n        return a.modified.compareTo(b.modified);\n    }\n  });\n\n  // 設定されている場合はフォルダを先に表示\n  if (showFoldersFirst) {\n    final dirs = files.where((f) => f.isDirectory);\n    final regular = files.where((f) => !f.isDirectory);\n    return [...dirs, ...regular];\n  }\n\n  return files;\n}\n```\n\n### ファイルメタデータ\n\n```dart\nclass SftpFile {\n  final String name;\n  final String path;\n  final int size;           // バイト\n  final int modified;       // Unix タイムスタンプ\n  final String permissions;  // 例: \"rwxr-xr-x\"\n  final String owner;\n  final String group;\n  final bool isDirectory;\n  final bool isSymlink;\n\n  String get sizeFormatted => formatBytes(size);\n  String get modifiedFormatted => formatDate(modified);\n}\n```\n\n## ファイル操作\n\n### アップロード\n\n```dart\nFuture<void> uploadFile(\n  String localPath,\n  String remotePath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // リクエストを作成\n  final req = SftpReq(\n    spi: spi,\n    remotePath: remotePath,\n    localPath: localPath,\n    type: SftpReqType.upload,\n  );\n\n  // キューに追加\n  _transferQueue.add(req);\n\n  // プログレス付きで転送を実行\n  final file = File(localPath);\n  final size = await file.length();\n  final stream = file.openRead();\n\n  await sftp.upload(\n    stream: stream,\n    toPath: remotePath,\n    onProgress: (transferred) {\n      _updateProgress(req, transferred, size);\n    },\n  );\n\n  // 完了\n  _transferQueue.remove(req);\n}\n```\n\n### ダウンロード\n\n```dart\nFuture<void> downloadFile(\n  String remotePath,\n  String localPath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // ローカルファイルを作成\n  final file = File(localPath);\n  final sink = file.openWrite();\n\n  // プログレス付きでダウンロード\n  final stat = await sftp.stat(remotePath);\n\n  await sftp.download(\n    fromPath: remotePath,\n    toSink: sink,\n    onProgress: (transferred) {\n      _updateProgress(\n        SftpReq(...),\n        transferred,\n        stat.size,\n      );\n    },\n  );\n\n  await sink.close();\n}\n```\n\n### 権限の編集\n\n```dart\nFuture<void> setPermissions(\n  String path,\n  String permissions,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 権限をパース (例: \"rwxr-xr-x\" または \"755\")\n  final mode = parsePermissions(permissions);\n\n  // SSH コマンド経由で設定 (SFTP より信頼性が高い)\n  final ssh = await getSshClient(spiId);\n  await ssh.exec('chmod $mode \"$path\"');\n}\n```\n\n## パス管理\n\n### パス構造\n\n```dart\nclass PathWithPrefix {\n  final String prefix;  // 例: \"/home/user\"\n  final String path;    // 相対パスまたは絶対パス\n\n  String get fullPath {\n    if (path.startsWith('/')) {\n      return path;  // 絶対パス\n    }\n    return '$prefix/$path';  // 相対パス\n  }\n\n  PathWithPrefix cd(String subPath) {\n    return PathWithPrefix(\n      prefix: fullPath,\n      path: subPath,\n    );\n  }\n}\n```\n\n### ナビゲーション履歴\n\n```dart\nclass PathHistory {\n  final List<String> _history = [];\n  int _index = -1;\n\n  void push(String path) {\n    // 前方の履歴を削除\n    _history.removeRange(_index + 1, _history.length);\n    _history.add(path);\n    _index = _history.length - 1;\n  }\n\n  String? back() {\n    if (_index > 0) {\n      _index--;\n      return _history[_index];\n    }\n    return null;\n  }\n\n  String? forward() {\n    if (_index < _history.length - 1) {\n      _index++;\n      return _history[_index];\n    }\n    return null;\n  }\n}\n```\n\n## 転送システム\n\n### 転送リクエスト\n\n```dart\nclass SftpReq {\n  final Spi spi;\n  final String remotePath;\n  final String localPath;\n  final SftpReqType type;\n  final DateTime createdAt;\n\n  int? totalBytes;\n  int? transferredBytes;\n  String? error;\n}\n```\n\n### プログレス追跡\n\n```dart\nclass TransferProgress {\n  final SftpReq request;\n  final int total;\n  final int transferred;\n  final DateTime startTime;\n\n  double get percentage => (transferred / total) * 100;\n  Duration get elapsed => DateTime.now().difference(startTime);\n\n  String get speedFormatted {\n    final bytesPerSecond = transferred / elapsed.inSeconds;\n    return formatSpeed(bytesPerSecond);\n  }\n}\n```\n\n### キュー管理\n\n```dart\nclass TransferQueue {\n  final List<SftpReq> _queue = [];\n  final Map<String, TransferProgress> _progress = {};\n  int _concurrent = 3;  // 最大同時転送数\n\n  Future<void> process() async {\n    final active = _progress.values.where((p) => p.isInProgress);\n    if (active.length >= _concurrent) return;\n\n    final pending = _queue.where((r) => !_progress.containsKey(r.id));\n    for (final req in pending.take(_concurrent - active.length)) {\n      _executeTransfer(req);\n    }\n  }\n\n  Future<void> _executeTransfer(SftpReq req) async {\n    try {\n      _progress[req.id] = TransferProgress.inProgress(req);\n\n      if (req.type == SftpReqType.upload) {\n        await uploadFile(req.localPath, req.remotePath);\n      } else {\n        await downloadFile(req.remotePath, req.localPath);\n      }\n\n      _progress[req.id] = TransferProgress.completed(req);\n    } catch (e) {\n      _progress[req.id] = TransferProgress.failed(req, e);\n    }\n  }\n}\n```\n\n## ローカルストレージパターン\n\n### ダウンロードキャッシュ\n\nダウンロードされたファイルは以下に保存されます。\n\n```dart\nString getLocalDownloadPath(String spiId, String remotePath) {\n  final normalized = remotePath.replaceAll('/', '_');\n  return 'Paths.file/$spiId/$normalized';\n}\n```\n\n例:\n- リモート: `/var/log/nginx/access.log`\n- spiId: `server-123`\n- ローカル: `Paths.file/server-123/_var_log_nginx_access.log`\n\n## ファイル編集\n\n### 編集ワークフロー\n\n```dart\nFuture<void> editFile(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 1. サイズチェック\n  final stat = await sftp.stat(path);\n  if (stat.size > editorMaxSize) {\n    showWarning('ファイルが大きすぎるため、内蔵エディタで開けません');\n    return;\n  }\n\n  // 2. 一時ディレクトリにダウンロード\n  final temp = await downloadToTemp(path);\n\n  // 3. エディタで開く\n  final content = await openEditor(temp.path);\n\n  // 4. アップロードして戻す\n  await uploadFile(temp.path, path);\n\n  // 5. クリーンアップ\n  await temp.delete();\n}\n```\n\n### 外部エディタとの統合\n\n```dart\nFuture<void> editInExternalEditor(String path) async {\n  final ssh = await getSshClient(spiId);\n\n  // エディタを使用してターミナルを開く\n  final editor = getSetting('sftpEditor', 'vim');\n  await ssh.exec('$editor \"$path\"');\n\n  // ユーザーがターミナルで編集\n  // 保存後、SFTP ビューを更新\n}\n```\n\n## エラーハンドリング\n\n### 権限エラー\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpPermissionException {\n  showError('アクセスが拒否されました: ${stat.path}');\n  showHint('ファイルの権限と所有者を確認してください');\n}\n```\n\n### 接続エラー\n\n```dart\ntry {\n  await sftp.listDir(path);\n} on SftpConnectionException {\n  showError('接続が失われました');\n  await reconnect();\n}\n```\n\n### スペースエラー\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpNoSpaceException {\n  showError('リモートサーバーのディスク容量が不足しています');\n}\n```\n\n## パフォーマンスの最適化\n\n### ディレクトリキャッシュ\n\n```dart\nclass DirectoryCache {\n  final Map<String, CachedDirectory> _cache = {};\n  final Duration ttl = Duration(minutes: 5);\n\n  Future<List<SftpFile>> list(String path) async {\n    final cached = _cache[path];\n    if (cached != null && !cached.isExpired) {\n      return cached.files;\n    }\n\n    final files = await sftp.listDir(path);\n    _cache[path] = CachedDirectory(files);\n    return files;\n  }\n}\n```\n\n### レイジーロード\n\n巨大なディレクトリ (>1000 アイテム) の場合:\n\n```dart\nList<SftpFile> loadPage(String path, int page, int pageSize) {\n  final all = cache[path] ?? [];\n  final start = page * pageSize;\n  final end = start + pageSize;\n  return all.sublist(start, end.clamp(0, all.length));\n}\n```\n\n### ページネーション\n\n```dart\nclass PaginatedDirectory {\n  static const pageSize = 100;\n\n  Future<List<SftpFile>> getPage(int page) async {\n    final offset = page * pageSize;\n    return await sftp.listDir(\n      path,\n      offset: offset,\n      limit: pageSize,\n    );\n  }\n}\n```\n"
  },
  {
    "path": "docs/src/content/docs/ja/principles/ssh.md",
    "content": "---\ntitle: SSH 接続\ndescription: SSH 接続の確立と管理の仕組み\n---\n\nServer Box における SSH 接続の仕組みについて解説します。\n\n## 接続フロー\n\n```text\nユーザー入力 → Spi 構成 → genClient() → SSH クライアント → セッション\n```\n\n### ステップ 1: 構成\n\n`Spi` (Server Parameter Info) モデルには以下が含まれます。\n\n```dart\nclass Spi {\n  String id;         // ユーザー ID / 一意の識別子\n  String name;       // サーバー名\n  String ip;         // IP アドレス\n  int port;          // SSH ポート (デフォルト 22)\n  String user;       // ユーザー名\n  String? pwd;       // パスワード (暗号化済み)\n  String? keyId;     // SSH キー ID\n  String? jumpId;    // 踏み台サーバー ID\n  String? alterUrl;  // 代替 URL\n}\n```\n\n### ステップ 2: クライアントの生成\n\n`genClient(spi)` が SSH クライアントを作成します。\n\n```dart\nFuture<SSHClient> genClient(Spi spi) async {\n  // 1. ソケットを確立\n  var socket = await connect(spi.ip, spi.port);\n\n  // 2. 失敗した場合は代替 URL を試行\n  if (socket == null && spi.alterUrl != null) {\n    socket = await connect(spi.alterUrl, spi.port);\n  }\n\n  if (socket == null) {\n    throw ConnectionException('Unable to connect');\n  }\n\n  // 3. 認証\n  final client = SSHClient(\n    socket: socket,\n    username: spi.user,\n    onPasswordRequest: () => spi.pwd,\n    onIdentityRequest: () => loadKey(spi.keyId),\n  );\n\n  // 4. ホストキーを検証\n  await verifyHostKey(client, spi);\n\n  return client;\n}\n```\n\n### ステップ 3: 踏み台サーバー (設定されている場合)\n\n踏み台サーバーを経由する場合、再帰的に接続します。\n\n```dart\nif (spi.jumpId != null) {\n  final jumpClient = await genClient(getJumpSpi(spi.jumpId));\n  final forwarded = await jumpClient.forwardLocal(\n    spi.ip,\n    spi.port,\n  );\n  // 転送されたソケット経由で接続\n}\n```\n\n## 認証方法\n\n### パスワード認証\n\n```dart\nonPasswordRequest: () => spi.pwd\n```\n\n- パスワードは Hive に暗号化して保存されます。\n- 接続時に復号されます。\n- 検証のためにサーバーに送信されます。\n\n### 公開鍵認証\n\n```dart\nonIdentityRequest: () async {\n  final key = await KeyStore.get(spi.keyId);\n  return decyptPem(key.pem, key.password);\n}\n```\n\n**キーのロードプロセス:**\n1. `KeyStore` から暗号化されたキーを取得\n2. パスワードを復号 (生体認証または入力)\n3. PEM 形式をパース\n4. 改行コードを標準化 (LF)\n5. 認証用に返却\n\n### キーボードインタラクティブ (Keyboard-Interactive)\n\n```dart\nonUserInfoRequest: (instructions) async {\n  // チャレンジ・レスポンスを処理\n  return responses;\n}\n```\n\n以下をサポートしています。\n- パスワード認証\n- OTP トークン\n- 二要素認証 (2FA)\n\n## ホストキー検証\n\n### なぜホストキーを検証するのか？\n\n正しいサーバーに接続していることを確認することで、**中間者攻撃 (MITM)** を防ぎます。\n\n### 保存形式\n\n```text\n{spi.id}::{keyType}\n```\n\n例:\n```text\nmy-server::ssh-ed25519\nmy-server::ecdsa-sha2-nistp256\n```\n\n### フィンガープリント形式\n\n**MD5 Hex:**\n```text\naa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99\n```\n\n**Base64:**\n```text\nSHA256:AbCdEf1234567890...=\n```\n\n### 検証フロー\n\n```dart\nFuture<void> verifyHostKey(SSHClient client, Spi spi) async {\n  final key = await client.hostKey;\n  final keyType = key.type;\n  final fingerprint = md5Hex(key); // または base64\n\n  final stored = SettingStore.sshKnownHostsFingerprints\n      ['${spi.id}::$keyType'];\n\n  if (stored == null) {\n    // 未知のホスト - ユーザーに確認\n    final trust = await promptUser(\n      '未知のホスト',\n      'フィンガープリント: $fingerprint',\n    );\n    if (trust) {\n      SettingStore.sshKnownHostsFingerprints\n          ['${spi.id}::$keyType'] = fingerprint;\n    }\n  } else if (stored != fingerprint) {\n    // 変更されている - ユーザーに警告\n    await warnUser(\n      'ホストキーが変更されています！',\n      '中間者攻撃の可能性があります',\n    );\n  }\n}\n```\n\n## セッション管理\n\n### 接続のプーリング\n\n`ServerProvider` でアクティブなクライアントを維持します。\n\n```dart\nclass ServerProvider {\n  final Map<String, SSHClient> _clients = {};\n\n  SSHClient getClient(String spiId) {\n    return _clients[spiId] ??= connect(spiId);\n  }\n}\n```\n\n### Keep-Alive\n\nアイドル中の接続を維持します。\n\n```dart\nTimer.periodic(\n  Duration(seconds: 30),\n  (_) => client.sendKeepAlive(),\n);\n```\n\n### 自動再接続\n\n接続が失われた場合:\n\n```dart\nclient.onError.listen((error) async {\n  await Future.delayed(Duration(seconds: 5));\n  reconnect();\n});\n```\n\n## 接続ライフサイクル\n\n```text\n┌─────────────┐\n│    初期状態 │\n└──────┬──────┘\n       │ connect()\n       ↓\n┌─────────────┐\n│    接続中   │ ←──┐\n└──────┬──────┘   │\n       │ 成功     │\n       ↓          │ 失敗 (再試行)\n┌─────────────┐   │\n│   接続済み  │───┘\n└──────┬──────┘\n       │\n       ↓\n┌─────────────┐\n│   アクティブ│ ──→ コマンド送信\n└──────┬──────┘\n       │\n       ↓ (エラー/切断)\n┌─────────────┐\n│    切断済み │\n└─────────────┘\n```\n\n## エラーハンドリング\n\n### 接続タイムアウト\n\n```dart\ntry {\n  await client.connect().timeout(\n    Duration(seconds: 30),\n  );\n} on TimeoutException {\n  throw ConnectionException('接続タイムアウト');\n}\n```\n\n### 認証失敗\n\n```dart\nonAuthFail: (error) {\n  if (error.contains('password')) {\n    return 'パスワードが正しくありません';\n  } else if (error.contains('key')) {\n    return 'SSH キーが正しくありません';\n  }\n  return '認証に失敗しました';\n}\n```\n\n### ホストキーの不一致\n\n```dart\nonHostKeyMismatch: (stored, current) {\n  showSecurityWarning(\n    'ホストキーが変更されています！',\n    '中間者攻撃の可能性があります',\n  );\n}\n```\n\n## パフォーマンスに関する考慮事項\n\n### 接続の再利用\n\n- 機能間でクライアントを再利用する\n- 不必要に切断・再接続を行わない\n- 並行操作のために接続をプーリングする\n\n### 最適な設定\n\n- **タイムアウト**: 30 秒 (調整可能)\n- **Keep-alive**: 30 秒ごと\n- **再試行遅延**: 5 秒\n\n### ネットワーク効率\n\n- 1 つの接続で複数の操作を行う\n- 可能な場合はコマンドをパイプライン化する\n- 複数の接続を同時に開くのを避ける\n"
  },
  {
    "path": "docs/src/content/docs/ja/principles/state.md",
    "content": "---\ntitle: 状態管理\ndescription: Riverpod を使用した状態管理の仕組み\n---\n\nServer Box における状態管理アーキテクチャについて解説します。\n\n## なぜ Riverpod なのか？\n\n**主な利点:**\n- **コンパイル時の安全性**: エラーをコンパイル時に検知可能\n- **BuildContext が不要**: どこからでも状態にアクセス可能\n- **テストの容易さ**: Provider を単体で簡単にテスト可能\n- **コード生成**: ボイラープレートを削減し、型安全性を確保\n\n## Provider アーキテクチャ\n\n```\n┌─────────────────────────────────────────────┐\n│          UI レイヤー (Widget)               │\n│  - ConsumerWidget / ConsumerStatefulWidget  │\n│  - ref.watch() / ref.read()                 │\n└─────────────────────────────────────────────┘\n                ↓ 監視 (watch)\n┌─────────────────────────────────────────────┐\n│          Provider レイヤー                  │\n│  - @riverpod アノテーション                 │\n│  - 生成された *.g.dart ファイル              │\n└─────────────────────────────────────────────┘\n                ↓ 使用 (use)\n┌─────────────────────────────────────────────┐\n│          Service / Store レイヤー           │\n│  - ビジネスロジック                         │\n│  - データアクセス                           │\n└─────────────────────────────────────────────┘\n```\n\n## 使用されている Provider の種類\n\n### 1. StateProvider (単純な状態)\n\n単純で観察可能な状態に使用します。\n\n```dart\n@riverpod\nclass ThemeNotifier extends _$ThemeNotifier {\n  @override\n  ThemeMode build() {\n    // 設定からロード\n    return SettingStore.themeMode;\n  }\n\n  void setTheme(ThemeMode mode) {\n    state = mode;\n    SettingStore.themeMode = mode;  // 永続化\n  }\n}\n```\n\n**使い方:**\n```dart\nclass MyWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final theme = ref.watch(themeNotifierProvider);\n    return Text('現在のテーマ: $theme');\n  }\n}\n```\n\n### 2. AsyncNotifierProvider (非同期状態)\n\n非同期にロードされるデータに使用します。\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    // 初回ロード\n    return await fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() async {\n      return await fetchStatus(server);\n    });\n  }\n}\n```\n\n**使い方:**\n```dart\nfinal status = ref.watch(serverStatusProvider(server));\n\nstatus.when(\n  data: (data) => StatusWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 3. StreamProvider (リアルタイムデータ)\n\n継続的なデータストリームに使用します。\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  final client = ref.watch(sshClientProvider(server));\n  final stream = client.monitorCpu();\n\n  // 監視されなくなったときに自動でリソースを解放\n  ref.onDispose(() {\n    client.stopMonitoring();\n  });\n\n  return stream;\n}\n```\n\n**使い方:**\n```dart\nfinal cpu = ref.watch(cpuUsageProvider(server));\n\ncpu.when(\n  data: (usage) => CpuChart(usage),\n  loading: () => CircularProgressIndicator(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 4. Family Provider (パラメータ付き)\n\nパラメータを受け取る Provider です。\n\n```dart\n@riverpod\nFuture<List<Container>> containers(ContainersRef ref, Server server) async {\n  final client = await ref.watch(sshClientProvider(server).future);\n  return await client.listContainers();\n}\n```\n\n**使い方:**\n```dart\nfinal containers = ref.watch(containersProvider(server));\n\n// サーバーが異なれば、キャッシュされる状態も異なります\nfinal containers2 = ref.watch(containersProvider(server2));\n```\n\n## パフォーマンスの最適化\n\n- **Provider Keep-Alive**: リスナーがいなくなっても破棄されないようにするには `@Riverpod(keepAlive: true)` を使用します。\n- **選択的な監視**: `select` を使用して、状態の特定の断片のみを監視します。\n- **Provider キャッシュ**: Family Provider はパラメータごとに結果をキャッシュします。\n\n## ベストプラクティス\n\n1. **Provider を近くに配置する**: 利用する Widget の近くに定義します。\n2. **コード生成を利用する**: 常に `@riverpod` を使用します。\n3. **Provider の責務を絞る**: 単一責任の原則に従います。\n4. **ロード状態を適切に扱う**: AsyncValue の各状態を必ずハンドルします。\n5. **リソースを解放する**: クリーンアップには `ref.onDispose()` を使用します。\n6. **深い Provider ツリーを避ける**: Provider のグラフ構造はフラットに保ちます。\n"
  },
  {
    "path": "docs/src/content/docs/ja/principles/terminal.md",
    "content": "---\ntitle: ターミナルの実装\ndescription: SSH ターミナルの内部的な仕組み\n---\n\nSSH ターミナルは、カスタマイズされた xterm.dart フォークをベースに構築された、最も複雑な機能の一つです。\n\n## アーキテクチャの概要\n\n```\n┌─────────────────────────────────────────────┐\n│          ターミナル UI レイヤー             │\n│  - タブ管理                                 │\n│  - 仮想キーボード                           │\n│  - テキスト選択                             │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         xterm.dart エミュレータ             │\n│  - PTY (擬似ターミナル)                     │\n│  - VT100/ANSI エミュレーション              │\n│  - レンダリングエンジン                     │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          SSH クライアントレイヤー           │\n│  - SSH セッション                           │\n│  - チャネル管理                             │\n│  - データストリーミング                     │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│            リモートサーバー                 │\n│  - シェルプロセス                           │\n│  - コマンド実行                             │\n└─────────────────────────────────────────────┘\n```\n\n## ターミナルセッションのライフサイクル\n\n### 1. セッションの作成\n\n```dart\nFuture<TerminalSession> createSession(Spi spi) async {\n  // 1. SSH クライアントを取得\n  final client = await genClient(spi);\n\n  // 2. PTY を開く\n  final pty = await client.openPty(\n    term: 'xterm-256color',\n    cols: 80,\n    rows: 24,\n  );\n\n  // 3. ターミナルエミュレータを初期化\n  final terminal = Terminal(\n    backend: PtyBackend(pty),\n  );\n\n  // 4. リサイズハンドラを設定\n  terminal.onResize.listen((size) {\n    pty.resize(size.cols, size.rows);\n  });\n\n  return TerminalSession(\n    terminal: terminal,\n    pty: pty,\n    client: client,\n  );\n}\n```\n\n### 2. ターミナルエミュレーション\n\nxterm.dart フォークは以下を提供します。\n\n**VT100/ANSI エミュレーション:**\n- カーソル移動\n- カラー（256 色対応）\n- テキスト属性（太字、アンダーラインなど）\n- スクロール領域\n- 代替画面バッファ\n\n**レンダリング:**\n- 行ベースのレンダリング\n- 双方向テキストのサポート\n- Unicode/Emoji 対応\n- 描画の最適化\n\n### 3. データフロー\n\n```\nユーザー入力\n    ↓\n仮想キーボード / 物理キーボード\n    ↓\nターミナルエミュレータ (キー → エスケープシーケンス)\n    ↓\nSSH チャネル (送信)\n    ↓\nリモート PTY\n    ↓\nリモートシェル\n    ↓\nコマンド出力\n    ↓\nSSH チャネル (受信)\n    ↓\nターミナルエミュレータ (ANSI コードのパース)\n    ↓\n画面へのレンダリング\n```\n\n## マルチタブシステム\n\n### タブ管理\n\nタブは画面遷移の間も状態を維持します。\n- SSH 接続の維持\n- ターミナル状態の保存\n- スクロールバッファの保持\n- 入力履歴の保持\n\n## 仮想キーボード\n\n### プラットフォーム固有の実装\n\n**iOS:**\n- UIView ベースのカスタムキーボード\n- キーボードボタンによる切り替え\n- フォーカスに基づいた自動表示/非表示\n\n**Android:**\n- カスタム入力メソッド\n- システムキーボードとの統合\n- クイックアクションボタン\n\n### キーボードボタン\n\n| ボタン | アクション |\n|--------|--------|\n| **切替** | システムキーボードの表示/非表示 |\n| **Ctrl** | Ctrl 修飾キーの送信 |\n| **Alt** | Alt 修飾キーの送信 |\n| **SFTP** | 現在のディレクトリを開く |\n| **クリップボード** | コンテキストに応じたコピー/ペースト |\n| **スニペット** | スニペットの実行 |\n\n## テキスト選択\n\n1. **長押し**: 選択モードに入る\n2. **ドラッグ**: 選択範囲を広げる\n3. **離す**: クリップボードにコピー\n\n## フォントと寸法\n\n### サイズ計算\n\n```dart\nclass TerminalDimensions {\n  static Size calculate(double fontSize, Size screenSize) {\n    final charWidth = fontSize * 0.6;  // 等幅フォントのアスペクト比\n    final charHeight = fontSize * 1.2;\n\n    final cols = (screenSize.width / charWidth).floor();\n    final rows = (screenSize.height / charHeight).floor();\n\n    return Size(cols.toDouble(), rows.toDouble());\n  }\n}\n```\n\n### ピンチズーム\n\n```dart\nGestureDetector(\n  onScaleStart: () => _baseFontSize = currentFontSize,\n  onScaleUpdate: (details) {\n    final newFontSize = _baseFontSize * details.scale;\n    resize(newFontSize);\n  },\n)\n```\n\n## カラースキーム\n\n- **ライト (Light)**: 明るい背景、暗いテキスト\n- **ダーク (Dark)**: 暗い背景、明るいテキスト\n- **AMOLED**: 真っ黒な背景\n\n## パフォーマンスの最適化\n\n- **Dirty rectangle**: 変更された領域のみを再描画\n- **行キャッシュ**: レンダリングされた行をキャッシュ\n- **レイジースクロール**: 長いバッファのための仮想スクロール\n- **バッチ更新**: 複数の書き込みを統合\n- **圧縮**: スクロールバッファの圧縮\n- **デバウンス**: 素早い入力に対するデバウンス処理\n"
  },
  {
    "path": "docs/src/content/docs/ja/quick-start.mdx",
    "content": "---\ntitle: クイックスタート\ndescription: Server Box を数分で使い始める\n---\n\nこのクイックスタートガイドに従って、最初のサーバーに接続し、監視を開始しましょう。\n\n## ステップ 1: サーバーを追加する\n\n1. Server Box を開きます\n2. **+** ボタンをタップして新しいサーバーを追加します\n3. サーバー情報を入力します：\n   - **名前**: サーバーの分かりやすい名前\n   - **ホスト**: IP アドレスまたはドメイン名\n   - **ポート**: SSH ポート（デフォルト：22）\n   - **ユーザー**: SSH ユーザー名\n   - **パスワードまたは鍵**: 認証方法\n\n4. **保存** をタップしてサーバーを追加します\n\n## ステップ 2: 接続と監視\n\n1. サーバーカードをタップして接続します\n2. アプリが SSH 接続を確立します\n3. 以下のリアルタイムステータスが表示されます：\n   - CPU 使用率\n   - メモリ (RAM) とスワップ\n   - ディスク使用量\n   - ネットワーク速度\n\n## ステップ 3: 機能を探索する\n\n接続すると、以下のことができます：\n\n- **ターミナルを開く**: ターミナルボタンをタップしてフル SSH アクセスを利用する\n- **ファイルを閲覧する**: SFTP を使用してファイルを管理する\n- **コンテナを管理する**: Docker コンテナを表示および制御する\n- **プロセスを表示する**: 実行中のプロセスを確認する\n- **スニペットを実行する**: 保存されたコマンドを実行する\n\n## ヒント\n\n- **生体認証**: Face ID / Touch ID / 指紋認証を有効にして、すばやくアクセス（モバイル）\n- **ホーム画面ウィジェット**: サーバーのステータスウィジェットをホーム画面に追加（iOS/Android）\n- **バックグラウンド実行**: バックグラウンドで接続を維持（Android）\n"
  },
  {
    "path": "docs/src/content/docs/platforms/desktop.md",
    "content": "---\ntitle: Desktop Features\ndescription: macOS, Linux, and Windows specific features\n---\n\nServer Box on desktop platforms provides additional productivity features.\n\n## macOS\n\n### Menu Bar Integration\n\n- Quick server status in menu bar\n- One-click server access\n- Compact mode for minimal distraction\n- Native macOS menu bar styling\n\n### Window State Persistence\n\n- Remembers window position and size\n- Restore previous session on launch\n- Multiple monitor support\n\n### Native Features\n\n- **Title Bar**: Custom or system title bar option\n- **Full Screen Mode**: Dedicated server monitoring\n- **Keyboard Shortcuts**: macOS-native shortcuts\n- **Touch Bar** (supported devices): Quick actions\n\n## Linux\n\n### Native Integration\n\n- System tray support\n- Desktop notification integration\n- File picker integration\n\n### Window Management\n\n- X11 and Wayland support\n- Tiling window manager friendly\n- Custom window decorations option\n\n## Windows\n\n### Features\n\n- System tray integration\n- Jump List quick actions\n- Native window controls\n- Auto-start on boot option\n\n## Cross-Platform Desktop Features\n\n### Keyboard Shortcuts\n\n- **Cmd/Ctrl + N**: New server\n- **Cmd/Ctrl + W**: Close tab\n- **Cmd/Ctrl + T**: New terminal tab\n- **Cmd/Ctrl + ,**: Settings\n\n### Themes\n\n- Light theme\n- Dark theme\n- AMOLED theme (pure black)\n- System theme (follows OS)\n\n### Multiple Windows\n\n- Open multiple servers in separate windows\n- Drag tabs to new window\n- Compare server stats side-by-side\n\n### Advantages Over Mobile\n\n- Larger screen for monitoring\n- Full keyboard for terminal\n- Faster file operations\n- Better multitasking\n"
  },
  {
    "path": "docs/src/content/docs/platforms/mobile.md",
    "content": "---\ntitle: Mobile Features\ndescription: iOS and Android specific features\n---\n\nServer Box provides several mobile-specific features for iOS and Android devices.\n\n## Biometric Authentication\n\nSecure your servers with biometric authentication:\n\n- **iOS**: Face ID or Touch ID\n- **Android**: Fingerprint authentication\n\nEnable in Settings > Security > Biometric Authentication\n\n## Home Screen Widgets\n\nAdd server status widgets to your home screen for quick monitoring.\n\n### iOS\n\n- Long press on home screen\n- Tap **+** to add widget\n- Search for \"Server Box\"\n- Choose widget size:\n  - Small: Single server status\n  - Medium: Multiple servers\n  - Large: Detailed info\n\n### Android\n\n- Long press on home screen\n- Tap **Widgets**\n- Find \"Server Box\"\n- Select widget type\n\n## Background Running\n\n### Android\n\nKeep connections alive in the background:\n\n- Enable in Settings > Advanced > Background Running\n- Requires battery optimization exclusion\n- Persistent notifications for active connections\n\n### iOS\n\nBackground limitations apply:\n\n- Connections may pause in background\n- Quick reconnect on return to app\n- Background refresh support\n\n## Push Notifications\n\nReceive notifications for:\n\n- Server offline alerts\n- High resource usage warnings\n- Task completion alerts\n\nConfigure in Settings > Notifications\n\n## Mobile UI Features\n\n- **Pull to Refresh**: Update server status\n- **Swipe Actions**: Quick server operations\n- **Landscape Mode**: Better terminal experience\n- **Virtual Keyboard**: Terminal shortcuts\n\n## File Integration\n\n- **Files App (iOS)**: Direct SFTP access from Files\n- **Storage Access Framework (Android)**: Share files with other apps\n- **Document Picker**: Easy file selection\n"
  },
  {
    "path": "docs/src/content/docs/principles/architecture.md",
    "content": "---\ntitle: Architecture Overview\ndescription: High-level application architecture\n---\n\nServer Box follows a layered architecture with clear separation of concerns.\n\n## Architecture Layers\n\n```\n┌─────────────────────────────────────────────────┐\n│          Presentation Layer (UI)                │\n│          lib/view/page/, lib/view/widget/       │\n│  - Pages, Widgets, Controllers                   │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│         Business Logic Layer                    │\n│         lib/data/provider/                      │\n│  - Riverpod Providers, State Notifiers          │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│           Data Access Layer                     │\n│         lib/data/store/, lib/data/model/        │\n│  - Hive Stores, Data Models                     │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│         External Integration Layer              │\n│  - SSH (dartssh2), Terminal (xterm), SFTP       │\n│  - Platform-specific code (iOS, Android, etc.)  │\n└─────────────────────────────────────────────────┘\n```\n\n## Application Foundation\n\n### Main Entry Point\n\n`lib/main.dart` initializes the app:\n\n```dart\nvoid main() {\n  runApp(\n    ProviderScope(\n      child: MyApp(),\n    ),\n  );\n}\n```\n\n### Root Widget\n\n`MyApp` provides:\n- **Theme Management**: Light/dark theme switching\n- **Routing Configuration**: Navigation structure\n- **Provider Scope**: Dependency injection root\n\n### Home Page\n\n`HomePage` serves as navigation hub:\n- **Tabbed Interface**: Server, Snippet, Container, SSH\n- **State Management**: Per-tab state\n- **Navigation**: Feature access\n\n## Core Systems\n\n### State Management: Riverpod\n\n**Why Riverpod?**\n- Compile-time safety\n- Easy testing\n- No Build context dependency\n- Works across platforms\n\n**Provider Types Used:**\n- `StateProvider`: Simple mutable state\n- `AsyncNotifierProvider`: Loading/error/data states\n- `StreamProvider`: Real-time data streams\n- Future providers: One-time async operations\n\n### Data Persistence: Hive CE\n\n**Why Hive CE?**\n- No native code dependencies\n- Fast key-value storage\n- Type-safe with code generation\n- No manual field annotations needed\n\n**Stores:**\n- `SettingStore`: App preferences\n- `ServerStore`: Server configurations\n- `SnippetStore`: Command snippets\n- `KeyStore`: SSH keys\n\n### Immutable Models: Freezed\n\n**Benefits:**\n- Compile-time immutability\n- Union types for state\n- Built-in JSON serialization\n- CopyWith extensions\n\n## Cross-Platform Strategy\n\n### Plugin System\n\nFlutter plugins provide platform integration:\n\n| Platform | Integration Method |\n|----------|-------------------|\n| iOS | CocoaPods, Swift/Obj-C |\n| Android | Gradle, Kotlin/Java |\n| macOS | CocoaPods, Swift |\n| Linux | CMake, C++ |\n| Windows | CMake, C# |\n\n### Platform-Specific Features\n\n**iOS Only:**\n- Home screen widgets\n- Live Activities\n- Apple Watch companion\n\n**Android Only:**\n- Background service\n- Push notifications\n- File system access\n\n**Desktop Only:**\n- Menu bar integration\n- Multiple windows\n- Custom title bar\n\n## Custom Dependencies\n\n### dartssh2 Fork\n\nEnhanced SSH client with:\n- Better mobile support\n- Enhanced error handling\n- Performance optimizations\n\n### xterm.dart Fork\n\nTerminal emulator with:\n- Mobile-optimized rendering\n- Touch gesture support\n- Virtual keyboard integration\n\n### fl_lib\n\nShared utilities package with:\n- Common widgets\n- Extensions\n- Helper functions\n\n## Build System\n\n### fl_build Package\n\nCustom build system for:\n- Multi-platform builds\n- Code signing\n- Asset bundling\n- Version management\n\n### Build Process\n\n```\nmake.dart (version) → fl_build (build) → Platform output\n```\n\n1. **Pre-build**: Calculate version from Git\n2. **Build**: Compile for target platform\n3. **Post-build**: Package and sign\n\n## Data Flow Example\n\n### Server Status Update\n\n```\n1. Timer triggers →\n2. Provider calls service →\n3. Service executes SSH command →\n4. Response parsed to model →\n5. State updated →\n6. UI rebuilds with new data\n```\n\n### User Action Flow\n\n```\n1. User taps button →\n2. Widget calls provider method →\n3. Provider updates state →\n4. State change triggers rebuild →\n5. New state reflected in UI\n```\n\n## Security Architecture\n\n### Data Protection\n\n- **Passwords**: Encrypted with flutter_secure_storage\n- **SSH Keys**: Encrypted at rest\n- **Host Fingerprints**: Stored securely\n- **Session Data**: Not persisted\n\n### Connection Security\n\n- **Host Key Verification**: MITM detection\n- **Encryption**: Standard SSH encryption\n- **No Plain Text**: Sensitive data never stored plain\n"
  },
  {
    "path": "docs/src/content/docs/principles/sftp.md",
    "content": "---\ntitle: SFTP System\ndescription: How the SFTP file browser works\n---\n\nThe SFTP system provides file management capabilities over SSH.\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────┐\n│              SFTP UI Layer                  │\n│  - File browser (remote)                    │\n│  - File browser (local)                     │\n│  - Transfer queue                           │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          SFTP State Management              │\n│  - sftpProvider                             │\n│  - Path management                          │\n│  - Operation queue                          │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         SFTP Protocol Layer                 │\n│  - SSH subsystem                            │\n│  - File operations                          │\n│  - Directory listing                        │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│            SSH Transport                    │\n│  - Secure channel                           │\n│  - Data streaming                           │\n└─────────────────────────────────────────────┘\n```\n\n## Connection Establishment\n\n### SFTP Client Creation\n\n```dart\nFuture<SftpClient> createSftpClient(Spi spi) async {\n  // 1. Get SSH client (reuse if available)\n  final sshClient = await genClient(spi);\n\n  // 2. Open SFTP subsystem\n  final sftp = await sshClient.openSftp();\n\n  return sftp;\n}\n```\n\n### Connection Reuse\n\nSFTP reuses existing SSH connections:\n\n```dart\nclass ServerProvider {\n  SSHClient? _sshClient;\n  SftpClient? _sftpClient;\n\n  Future<SftpClient> getSftpClient(String spiId) async {\n    _sftpClient ??= await _sshClient!.openSftp();\n    return _sftpClient!;\n  }\n}\n```\n\n## File System Operations\n\n### Directory Listing\n\n```dart\nFuture<List<SftpFile>> listDirectory(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // List directory\n  final files = await sftp.listDir(path);\n\n  // Sort based on settings\n  files.sort((a, b) {\n    switch (sortOption) {\n      case SortOption.name:\n        return a.name.toLowerCase().compareTo(b.name.toLowerCase());\n      case SortOption.size:\n        return a.size.compareTo(b.size);\n      case SortOption.time:\n        return a.modified.compareTo(b.modified);\n    }\n  });\n\n  // Folders first if enabled\n  if (showFoldersFirst) {\n    final dirs = files.where((f) => f.isDirectory);\n    final regular = files.where((f) => !f.isDirectory);\n    return [...dirs, ...regular];\n  }\n\n  return files;\n}\n```\n\n### File Metadata\n\n```dart\nclass SftpFile {\n  final String name;\n  final String path;\n  final int size;           // Bytes\n  final int modified;       // Unix timestamp\n  final String permissions;  // e.g., \"rwxr-xr-x\"\n  final String owner;\n  final String group;\n  final bool isDirectory;\n  final bool isSymlink;\n\n  String get sizeFormatted => formatBytes(size);\n  String get modifiedFormatted => formatDate(modified);\n}\n```\n\n## File Operations\n\n### Upload\n\n```dart\nFuture<void> uploadFile(\n  String localPath,\n  String remotePath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Create request\n  final req = SftpReq(\n    spi: spi,\n    remotePath: remotePath,\n    localPath: localPath,\n    type: SftpReqType.upload,\n  );\n\n  // Add to queue\n  _transferQueue.add(req);\n\n  // Execute transfer with progress\n  final file = File(localPath);\n  final size = await file.length();\n  final stream = file.openRead();\n\n  await sftp.upload(\n    stream: stream,\n    toPath: remotePath,\n    onProgress: (transferred) {\n      _updateProgress(req, transferred, size);\n    },\n  );\n\n  // Complete\n  _transferQueue.remove(req);\n}\n```\n\n### Download\n\n```dart\nFuture<void> downloadFile(\n  String remotePath,\n  String localPath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Create local file\n  final file = File(localPath);\n  final sink = file.openWrite();\n\n  // Download with progress\n  final stat = await sftp.stat(remotePath);\n\n  await sftp.download(\n    fromPath: remotePath,\n    toSink: sink,\n    onProgress: (transferred) {\n      _updateProgress(\n        SftpReq(...),\n        transferred,\n        stat.size,\n      );\n    },\n  );\n\n  await sink.close();\n}\n```\n\n### Permission Editing\n\n```dart\nFuture<void> setPermissions(\n  String path,\n  String permissions,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // Parse permissions (e.g., \"rwxr-xr-x\" or \"755\")\n  final mode = parsePermissions(permissions);\n\n  // Set via SSH command (more reliable than SFTP)\n  final ssh = await getSshClient(spiId);\n  await ssh.exec('chmod $mode \"$path\"');\n}\n```\n\n## Path Management\n\n### Path Structure\n\n```dart\nclass PathWithPrefix {\n  final String prefix;  // e.g., \"/home/user\"\n  final String path;    // Relative or absolute\n\n  String get fullPath {\n    if (path.startsWith('/')) {\n      return path;  // Absolute path\n    }\n    return '$prefix/$path';  // Relative path\n  }\n\n  PathWithPrefix cd(String subPath) {\n    return PathWithPrefix(\n      prefix: fullPath,\n      path: subPath,\n    );\n  }\n}\n```\n\n### Navigation History\n\n```dart\nclass PathHistory {\n  final List<String> _history = [];\n  int _index = -1;\n\n  void push(String path) {\n    // Remove forward history\n    _history.removeRange(_index + 1, _history.length);\n    _history.add(path);\n    _index = _history.length - 1;\n  }\n\n  String? back() {\n    if (_index > 0) {\n      _index--;\n      return _history[_index];\n    }\n    return null;\n  }\n\n  String? forward() {\n    if (_index < _history.length - 1) {\n      _index++;\n      return _history[_index];\n    }\n    return null;\n  }\n}\n```\n\n## Transfer System\n\n### Transfer Request\n\n```dart\nclass SftpReq {\n  final Spi spi;\n  final String remotePath;\n  final String localPath;\n  final SftpReqType type;\n  final DateTime createdAt;\n\n  int? totalBytes;\n  int? transferredBytes;\n  String? error;\n}\n```\n\n### Progress Tracking\n\n```dart\nclass TransferProgress {\n  final SftpReq request;\n  final int total;\n  final int transferred;\n  final DateTime startTime;\n\n  double get percentage => (transferred / total) * 100;\n  Duration get elapsed => DateTime.now().difference(startTime);\n\n  String get speedFormatted {\n    final bytesPerSecond = transferred / elapsed.inSeconds;\n    return formatSpeed(bytesPerSecond);\n  }\n}\n```\n\n### Queue Management\n\n```dart\nclass TransferQueue {\n  final List<SftpReq> _queue = [];\n  final Map<String, TransferProgress> _progress = {};\n  int _concurrent = 3;  // Max concurrent transfers\n\n  Future<void> process() async {\n    final active = _progress.values.where((p) => p.isInProgress);\n    if (active.length >= _concurrent) return;\n\n    final pending = _queue.where((r) => !_progress.containsKey(r.id));\n    for (final req in pending.take(_concurrent - active.length)) {\n      _executeTransfer(req);\n    }\n  }\n\n  Future<void> _executeTransfer(SftpReq req) async {\n    try {\n      _progress[req.id] = TransferProgress.inProgress(req);\n\n      if (req.type == SftpReqType.upload) {\n        await uploadFile(req.localPath, req.remotePath);\n      } else {\n        await downloadFile(req.remotePath, req.localPath);\n      }\n\n      _progress[req.id] = TransferProgress.completed(req);\n    } catch (e) {\n      _progress[req.id] = TransferProgress.failed(req, e);\n    }\n  }\n}\n```\n\n## Local Storage Pattern\n\n### Download Cache\n\nDownloaded files stored at:\n\n```dart\nString getLocalDownloadPath(String spiId, String remotePath) {\n  final normalized = remotePath.replaceAll('/', '_');\n  return 'Paths.file/$spiId/$normalized';\n}\n```\n\nExample:\n- Remote: `/var/log/nginx/access.log`\n- spiId: `server-123`\n- Local: `Paths.file/server-123/_var_log_nginx_access.log`\n\n## File Editing\n\n### Edit Workflow\n\n```dart\nFuture<void> editFile(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 1. Check size\n  final stat = await sftp.stat(path);\n  if (stat.size > editorMaxSize) {\n    showWarning('File too large for built-in editor');\n    return;\n  }\n\n  // 2. Download to temp\n  final temp = await downloadToTemp(path);\n\n  // 3. Open in editor\n  final content = await openEditor(temp.path);\n\n  // 4. Upload back\n  await uploadFile(temp.path, path);\n\n  // 5. Cleanup\n  await temp.delete();\n}\n```\n\n### External Editor Integration\n\n```dart\nFuture<void> editInExternalEditor(String path) async {\n  final ssh = await getSshClient(spiId);\n\n  // Open terminal with editor\n  final editor = getSetting('sftpEditor', 'vim');\n  await ssh.exec('$editor \"$path\"');\n\n  // User edits in terminal\n  // After save, refresh SFTP view\n}\n```\n\n## Error Handling\n\n### Permission Errors\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpPermissionException {\n  showError('Permission denied: ${stat.path}');\n  showHint('Check file permissions and ownership');\n}\n```\n\n### Connection Errors\n\n```dart\ntry {\n  await sftp.listDir(path);\n} on SftpConnectionException {\n  showError('Connection lost');\n  await reconnect();\n}\n```\n\n### Space Errors\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpNoSpaceException {\n  showError('Disk full on remote server');\n}\n```\n\n## Performance Optimizations\n\n### Directory Caching\n\n```dart\nclass DirectoryCache {\n  final Map<String, CachedDirectory> _cache = {};\n  final Duration ttl = Duration(minutes: 5);\n\n  Future<List<SftpFile>> list(String path) async {\n    final cached = _cache[path];\n    if (cached != null && !cached.isExpired) {\n      return cached.files;\n    }\n\n    final files = await sftp.listDir(path);\n    _cache[path] = CachedDirectory(files);\n    return files;\n  }\n}\n```\n\n### Lazy Loading\n\nFor large directories (>1000 items):\n\n```dart\nList<SftpFile> loadPage(String path, int page, int pageSize) {\n  final all = cache[path] ?? [];\n  final start = page * pageSize;\n  final end = start + pageSize;\n  return all.sublist(start, end.clamp(0, all.length));\n}\n```\n\n### Pagination\n\n```dart\nclass PaginatedDirectory {\n  static const pageSize = 100;\n\n  Future<List<SftpFile>> getPage(int page) async {\n    final offset = page * pageSize;\n    return await sftp.listDir(\n      path,\n      offset: offset,\n      limit: pageSize,\n    );\n  }\n}\n```\n"
  },
  {
    "path": "docs/src/content/docs/principles/ssh.md",
    "content": "---\ntitle: SSH Connection\ndescription: How SSH connections are established and managed\n---\n\nUnderstanding SSH connections in Server Box.\n\n## Connection Flow\n\n```\nUser Input → Spi Config → genClient() → SSH Client → Session\n```\n\n### Step 1: Configuration\n\nThe `Spi` (Server Parameter Info) model contains:\n\n```dart\nclass Spi {\n  String name;       // Server name\n  String ip;         // IP address\n  int port;          // SSH port (default 22)\n  String user;       // Username\n  String? pwd;       // Password (encrypted)\n  String? keyId;     // SSH key ID\n  String? jumpId;    // Jump server ID\n  String? alterUrl;  // Alternative URL\n}\n```\n\n### Step 2: Client Generation\n\n`genClient(spi)` creates SSH client:\n\n```dart\nFuture<SSHClient> genClient(Spi spi) async {\n  // 1. Establish socket\n  final socket = await connect(spi.ip, spi.port);\n\n  // 2. Try alternative URL if failed\n  if (socket == null && spi.alterUrl != null) {\n    socket = await connect(spi.alterUrl, spi.port);\n  }\n\n  // 3. Authenticate\n  final client = SSHClient(\n    socket: socket,\n    username: spi.user,\n    onPasswordRequest: () => spi.pwd,\n    onIdentityRequest: () => loadKey(spi.keyId),\n  );\n\n  // 4. Verify host key\n  await verifyHostKey(client, spi);\n\n  return client;\n}\n```\n\n### Step 3: Jump Server (if configured)\n\nFor jump servers, recursive connection:\n\n```dart\nif (spi.jumpId != null) {\n  final jumpClient = await genClient(getJumpSpi(spi.jumpId));\n  final forwarded = await jumpClient.forwardLocal(\n    spi.ip,\n    spi.port,\n  );\n  // Connect through forwarded socket\n}\n```\n\n## Authentication Methods\n\n### Password Authentication\n\n```dart\nonPasswordRequest: () => spi.pwd\n```\n\n- Password stored encrypted in Hive\n- Decrypted on connection\n- Sent to server for verification\n\n### Private Key Authentication\n\n```dart\nonIdentityRequest: () async {\n  final key = await KeyStore.get(spi.keyId);\n  return decyptPem(key.pem, key.password);\n}\n```\n\n**Key Loading Process:**\n1. Retrieve encrypted key from `KeyStore`\n2. Decrypt password (biometric/prompt)\n3. Parse PEM format\n4. Standardize line endings (LF)\n5. Return for authentication\n\n### Keyboard-Interactive\n\n```dart\nonUserInfoRequest: (instructions) async {\n  // Handle challenge-response\n  return responses;\n}\n```\n\nSupports:\n- Password authentication\n- OTP tokens\n- Two-factor authentication\n\n## Host Key Verification\n\n### Why Verify Host Keys?\n\nPrevents **Man-in-the-Middle (MITM)** attacks by ensuring you're connecting to the same server.\n\n### Storage Format\n\n```\n{spi.id}::{keyType}\n```\n\nExample:\n```\nmy-server::ssh-ed25519\nmy-server::ecdsa-sha2-nistp256\n```\n\n### Fingerprint Formats\n\n**MD5 Hex:**\n```\naa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99\n```\n\n**Base64:**\n```\nSHA256:AbCdEf1234567890...=\n```\n\n### Verification Flow\n\n```dart\nFuture<void> verifyHostKey(SSHClient client, Spi spi) async {\n  final key = await client.hostKey;\n  final fingerprint = md5Hex(key); // or base64\n\n  final stored = SettingStore.sshKnownHostsFingerprints\n      ['$keyId::$keyType'];\n\n  if (stored == null) {\n    // New host - prompt user\n    final trust = await promptUser(\n      'Unknown host',\n      'Fingerprint: $fingerprint',\n    );\n    if (trust) {\n      SettingStore.sshKnownHostsFingerprints\n          ['$keyId::$keyType'] = fingerprint;\n    }\n  } else if (stored != fingerprint) {\n    // Changed - warn user\n    await warnUser(\n      'Host key changed!',\n      'Possible MITM attack',\n    );\n  }\n}\n```\n\n## Session Management\n\n### Connection Pooling\n\nActive clients maintained in `ServerProvider`:\n\n```dart\nclass ServerProvider {\n  final Map<String, SSHClient> _clients = {};\n\n  SSHClient getClient(String spiId) {\n    return _clients[spiId] ??= connect(spiId);\n  }\n}\n```\n\n### Keep-Alive\n\nMaintain connection during inactivity:\n\n```dart\nTimer.periodic(\n  Duration(seconds: 30),\n  (_) => client.sendKeepAlive(),\n);\n```\n\n### Auto-Reconnect\n\nOn connection loss:\n\n```dart\nclient.onError.listen((error) async {\n  await Future.delayed(Duration(seconds: 5));\n  reconnect();\n});\n```\n\n## Connection Lifecycle\n\n```\n┌─────────────┐\n│   Initial   │\n└──────┬──────┘\n       │ connect()\n       ↓\n┌─────────────┐\n│ Connecting  │ ←──┐\n└──────┬──────┘   │\n       │ success  │\n       ↓          │ fail (retry)\n┌─────────────┐   │\n│ Connected   │───┘\n└──────┬──────┘\n       │\n       ↓\n┌─────────────┐\n│   Active    │ ──→ Send commands\n└──────┬──────┘\n       │\n       ↓ (error/disconnect)\n┌─────────────┐\n│ Disconnected│\n└─────────────┘\n```\n\n## Error Handling\n\n### Connection Timeout\n\n```dart\ntry {\n  await client.connect().timeout(\n    Duration(seconds: 30),\n  );\n} on TimeoutException {\n  throw ConnectionException('Connection timeout');\n}\n```\n\n### Authentication Failure\n\n```dart\nonAuthFail: (error) {\n  if (error.contains('password')) {\n    return 'Invalid password';\n  } else if (error.contains('key')) {\n    return 'Invalid SSH key';\n  }\n  return 'Authentication failed';\n}\n```\n\n### Host Key Mismatch\n\n```dart\nonHostKeyMismatch: (stored, current) {\n  showSecurityWarning(\n    'Host key has changed!',\n    'Possible MITM attack',\n  );\n}\n```\n\n## Performance Considerations\n\n### Connection Reuse\n\n- Reuse clients across features\n- Don't disconnect/reconnect unnecessarily\n- Pool connections for concurrent operations\n\n### Optimal Settings\n\n- **Timeout**: 30 seconds (adjustable)\n- **Keep-alive**: Every 30 seconds\n- **Retry delay**: 5 seconds\n\n### Network Efficiency\n\n- Single connection for multiple operations\n- Pipeline commands when possible\n- Avoid opening multiple connections\n"
  },
  {
    "path": "docs/src/content/docs/principles/state.md",
    "content": "---\ntitle: State Management\ndescription: How state is managed with Riverpod\n---\n\nUnderstanding the state management architecture in Server Box.\n\n## Why Riverpod?\n\n**Key Benefits:**\n- **Compile-time safety**: Catch errors at compile time\n- **No BuildContext needed**: Access state anywhere\n- **Easy testing**: Simple to test providers in isolation\n- **Code generation**: Less boilerplate, type-safe\n\n## Provider Architecture\n\n```\n┌─────────────────────────────────────────────┐\n│         UI Layer (Widgets)                  │\n│  - ConsumerWidget / ConsumerStatefulWidget  │\n│  - ref.watch() / ref.read()                 │\n└─────────────────────────────────────────────┘\n                ↓ watches\n┌─────────────────────────────────────────────┐\n│         Provider Layer                      │\n│  - @riverpod annotations                    │\n│  - Generated *.g.dart files                 │\n└─────────────────────────────────────────────┘\n                ↓ uses\n┌─────────────────────────────────────────────┐\n│         Service / Store Layer               │\n│  - Business logic                           │\n│  - Data access                              │\n└─────────────────────────────────────────────┘\n```\n\n## Provider Types Used\n\n### 1. StateProvider (Simple State)\n\nFor simple, observable state:\n\n```dart\n@riverpod\nclass ThemeNotifier extends _$ThemeNotifier {\n  @override\n  ThemeMode build() {\n    // Load from settings\n    return SettingStore.themeMode;\n  }\n\n  void setTheme(ThemeMode mode) {\n    state = mode;\n    SettingStore.themeMode = mode;  // Persist\n  }\n}\n```\n\n**Usage:**\n```dart\nclass MyWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final theme = ref.watch(themeNotifierProvider);\n    return Text('Theme: $theme');\n  }\n}\n```\n\n### 2. AsyncNotifierProvider (Async State)\n\nFor data that loads asynchronously:\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    // Initial load\n    return await fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() async {\n      return await fetchStatus(server);\n    });\n  }\n}\n```\n\n**Usage:**\n```dart\nfinal status = ref.watch(serverStatusProvider(server));\n\nstatus.when(\n  data: (data) => StatusWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 3. StreamProvider (Real-time Data)\n\nFor continuous data streams:\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  final client = ref.watch(sshClientProvider(server));\n  final stream = client.monitorCpu();\n\n  // Auto-dispose when not watched\n  ref.onDispose(() {\n    client.stopMonitoring();\n  });\n\n  return stream;\n}\n```\n\n**Usage:**\n```dart\nfinal cpu = ref.watch(cpuUsageProvider(server));\n\ncpu.when(\n  data: (usage) => CpuChart(usage),\n  loading: () => CircularProgressIndicator(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 4. Family Providers (Parameterized)\n\nProviders that accept parameters:\n\n```dart\n@riverpod\nFuture<List<Container>> containers(ContainersRef ref, Server server) async {\n  final client = await ref.watch(sshClientProvider(server).future);\n  return await client.listContainers();\n}\n```\n\n**Usage:**\n```dart\nfinal containers = ref.watch(containersProvider(server));\n\n// Different servers = different cached states\nfinal containers2 = ref.watch(containersProvider(server2));\n```\n\n## State Update Patterns\n\n### Direct State Update\n\n```dart\nref.read(settingsProvider.notifier).updateTheme(darkMode);\n```\n\n### Computed State\n\n```dart\n@riverpod\nint totalServers(TotalServersRef ref) {\n  final servers = ref.watch(serversProvider);\n  return servers.length;\n}\n```\n\n### Derived State\n\n```dart\n@riverpod\nList<Server> onlineServers(OnlineServersRef ref) {\n  final all = ref.watch(serversProvider);\n  return all.where((s) => s.isOnline).toList();\n}\n```\n\n## Server-Specific State\n\n### Per-Server Providers\n\nEach server has isolated state:\n\n```dart\n@riverpod\nclass ServerProvider extends _$ServerProvider {\n  @override\n  ServerState build(Server server) {\n    return ServerState.disconnected();\n  }\n\n  Future<void> connect() async {\n    state = ServerState.connecting();\n    try {\n      final client = await genClient(server.spi);\n      state = ServerState.connected(client);\n    } catch (e) {\n      state = ServerState.error(e.toString());\n    }\n  }\n}\n```\n\n### Provider Keys\n\n```dart\n// Unique provider per server\n@riverpod\nServerStatus serverStatus(ServerStatusRef ref, Server server) {\n  // server.id used as key\n}\n```\n\n## Reactive Patterns\n\n### Auto-Refresh\n\n```dart\n@riverpod\nclass AutoRefreshServerStatus extends _$AutoRefreshServerStatus {\n  Timer? _timer;\n\n  @override\n  Future<StatusModel> build(Server server) async {\n    // Start timer\n    _timer = Timer.periodic(Duration(seconds: 5), (_) {\n      refresh();\n    });\n\n    ref.onDispose(() {\n      _timer?.cancel();\n    });\n\n    return await fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() => fetchStatus(server));\n  }\n}\n```\n\n### Multi-Provider Dependencies\n\n```dart\n@riverpod\nFuture<SystemInfo> systemInfo(SystemInfoRef ref, Server server) async {\n  // Wait for SSH client first\n  final client = await ref.watch(sshClientProvider(server).future);\n\n  // Then fetch system info\n  return await client.getSystemInfo();\n}\n```\n\n## State Persistence\n\n### Hive Integration\n\n```dart\n@riverpod\nclass ServerStoreNotifier extends _$ServerStoreNotifier {\n  @override\n  List<Server> build() {\n    // Load from Hive\n    return Hive.box<Server>('servers').values.toList();\n  }\n\n  void addServer(Server server) {\n    state = [...state, server];\n    // Persist to Hive\n    Hive.box<Server>('servers').put(server.id, server);\n  }\n\n  void removeServer(String id) {\n    state = state.where((s) => s.id != id).toList();\n    // Remove from Hive\n    Hive.box<Server>('servers').delete(id);\n  }\n}\n```\n\n## Error Handling\n\n### Error States\n\n```dart\n@riverpod\nclass ConnectionManager extends _$ConnectionManager {\n  @override\n  ConnectionState build() {\n    return ConnectionState.idle();\n  }\n\n  Future<void> connect(Server server) async {\n    state = ConnectionState.connecting();\n    try {\n      final client = await genClient(server.spi);\n      state = ConnectionState.connected(client);\n    } on SocketException catch (e) {\n      state = ConnectionState.error('Network error: $e');\n    } on AuthenticationException catch (e) {\n      state = ConnectionState.error('Auth failed: $e');\n    } catch (e) {\n      state = ConnectionState.error('Unknown error: $e');\n    }\n  }\n}\n```\n\n### Error Recovery\n\n```dart\n@riverpod\nclass ResilientFetcher extends _$ResilientFetcher {\n  int _retryCount = 0;\n\n  @override\n  Future<Data> build(Server server) async {\n    return await _fetchWithRetry();\n  }\n\n  Future<Data> _fetchWithRetry() async {\n    try {\n      return await fetchData(server);\n    } catch (e) {\n      if (_retryCount < 3) {\n        _retryCount++;\n        await Future.delayed(Duration(seconds: 2));\n        return await _fetchWithRetry();\n      }\n      rethrow;\n    }\n  }\n}\n```\n\n## Performance Optimizations\n\n### Provider Keep-Alive\n\n```dart\n@Riverpod(keepAlive: true)  // Don't dispose when no listeners\nclass GlobalSettings extends _$GlobalSettings {\n  @override\n  Settings build() {\n    return Settings.defaults();\n  }\n}\n```\n\n### Selective Watching\n\n```dart\n// Watch only specific part of state\nfinal name = ref.watch(serverProvider.select((s) => s.name));\n```\n\n### Provider Caching\n\nFamily providers cache results per parameter:\n\n```dart\n// Cached per server ID\nfinal status1 = ref.watch(serverStatusProvider(server1));\nfinal status2 = ref.watch(serverStatusProvider(server2));\n// Different states, both cached\n```\n\n## Testing with Riverpod\n\n### Provider Container\n\n```dart\ntest('fetch server status', () async {\n  final container = ProviderContainer();\n  addTearDown(container.dispose);\n\n  // Override provider\n  container.overrideFactory(\n    sshClientProvider,\n    (ref, server) => MockSshClient(),\n  );\n\n  final status = await container.read(\n    serverStatusProvider(testServer).future,\n  );\n\n  expect(status, isA<StatusModel>());\n});\n```\n\n## Best Practices\n\n1. **Co-locate providers**: Place near consuming widgets\n2. **Use code generation**: Always use `@riverpod`\n3. **Keep providers focused**: Single responsibility\n4. **Handle loading states**: Always handle AsyncValue states\n5. **Dispose resources**: Use `ref.onDispose()` for cleanup\n6. **Avoid deep provider trees**: Keep provider graph flat\n"
  },
  {
    "path": "docs/src/content/docs/principles/terminal.md",
    "content": "---\ntitle: Terminal Implementation\ndescription: How the SSH terminal works internally\n---\n\nThe SSH terminal is one of the most complex features, built on a custom xterm.dart fork.\n\n## Architecture Overview\n\n```\n┌─────────────────────────────────────────────┐\n│          Terminal UI Layer                  │\n│  - Tab management                           │\n│  - Virtual keyboard                         │\n│  - Text selection                           │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         xterm.dart Emulator                 │\n│  - PTY (Pseudo Terminal)                    │\n│  - VT100/ANSI emulation                     │\n│  - Rendering engine                         │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          SSH Client Layer                   │\n│  - SSH session                              │\n│  - Channel management                       │\n│  - Data streaming                           │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          Remote Server                      │\n│  - Shell process                            │\n│  - Command execution                        │\n└─────────────────────────────────────────────┘\n```\n\n## Terminal Session Lifecycle\n\n### 1. Session Creation\n\n```dart\nFuture<TerminalSession> createSession(Spi spi) async {\n  // 1. Get SSH client\n  final client = await genClient(spi);\n\n  // 2. Create PTY\n  final pty = await client.openPty(\n    term: 'xterm-256color',\n    cols: 80,\n    rows: 24,\n  );\n\n  // 3. Initialize terminal emulator\n  final terminal = Terminal(\n    backend: PtyBackend(pty),\n  );\n\n  // 4. Setup resize handler\n  terminal.onResize.listen((size) {\n    pty.resize(size.cols, size.rows);\n  });\n\n  return TerminalSession(\n    terminal: terminal,\n    pty: pty,\n    client: client,\n  );\n}\n```\n\n### 2. Terminal Emulation\n\nThe xterm.dart fork provides:\n\n**VT100/ANSI Emulation:**\n- Cursor movement\n- Colors (256-color support)\n- Text attributes (bold, underline, etc.)\n- Scrolling regions\n- Alternate screen buffer\n\n**Rendering:**\n- Line-based rendering\n- Bidirectional text support\n- Unicode/emoji support\n- Optimized redraws\n\n### 3. Data Flow\n\n```\nUser Input\n    ↓\nVirtual Keyboard / Physical Keyboard\n    ↓\nTerminal Emulator (key → escape sequence)\n    ↓\nSSH Channel (send)\n    ↓\nRemote PTY\n    ↓\nRemote Shell\n    ↓\nCommand Output\n    ↓\nSSH Channel (receive)\n    ↓\nTerminal Emulator (parse ANSI codes)\n    ↓\nRender to Screen\n```\n\n## Multi-Tab System\n\n### Tab Management\n\n```dart\nclass TerminalTabs {\n  final Map<String, TabData> _tabs = {};\n  String? _activeTabId;\n\n  void createTab(Server server) {\n    final id = _generateTabId(server);\n    _tabs[id] = TabData(\n      id: id,\n      name: _generateTabName(server),\n      session: createSession(server),\n    );\n    _activeTabId = id;\n  }\n\n  String _generateTabName(Server server) {\n    final count = _tabs.values\n        .where((t) => t.name.startsWith(server.name))\n        .length;\n    return count == 0 ? server.name : '${server.name}($count)';\n  }\n}\n```\n\n### Session Persistence\n\nTabs maintain state across navigation:\n\n- SSH connection kept alive\n- Terminal state preserved\n- Scroll buffer maintained\n- Input history retained\n\n## Virtual Keyboard\n\n### Platform-Specific Implementation\n\n**iOS:**\n- UIView-based custom keyboard\n- Toggleable with keyboard button\n- Auto-show/hide based on focus\n\n**Android:**\n- Custom input method\n- Integrated with system keyboard\n- Quick action buttons\n\n### Keyboard Buttons\n\n| Button | Action |\n|--------|--------|\n| **Toggle** | Show/hide system keyboard |\n| **Ctrl** | Send Ctrl modifier |\n| **Alt** | Send Alt modifier |\n| **SFTP** | Open current directory |\n| **Clipboard** | Copy/Paste context-aware |\n| **Snippets** | Execute snippet |\n\n### Key Encoding\n\n```dart\nString encodeKey(Key key) {\n  switch (key) {\n    case Key.enter:\n      return '\\r';\n    case Key.tab:\n      return '\\t';\n    case Key.escape:\n      return '\\x1b';\n    case Key.ctrlC:\n      return '\\x03';\n    // ... more keys\n  }\n}\n```\n\n## Text Selection\n\n### Selection Mode\n\n1. **Long press**: Enter selection mode\n2. **Drag**: Extend selection\n3. **Release**: Copy to clipboard\n\n### Selection Storage\n\n```dart\nclass TextSelection {\n  final BufferRange range;\n  final String text;\n\n  void copyToClipboard() {\n    Clipboard.setData(ClipboardData(text: text));\n  }\n}\n```\n\n## Font and Dimensions\n\n### Size Calculation\n\n```dart\nclass TerminalDimensions {\n  static Size calculate(double fontSize, Size screenSize) {\n    final charWidth = fontSize * 0.6;  // Monospace aspect ratio\n    final charHeight = fontSize * 1.2;\n\n    final cols = (screenSize.width / charWidth).floor();\n    final rows = (screenSize.height / charHeight).floor();\n\n    return Size(cols.toDouble(), rows.toDouble());\n  }\n}\n```\n\n### Pinch-to-Zoom\n\n```dart\nGestureDetector(\n  onScaleStart: () => _baseFontSize = currentFontSize,\n  onScaleUpdate: (details) {\n    final newFontSize = _baseFontSize * details.scale;\n    resize(newFontSize);\n  },\n)\n```\n\n## Color Scheme\n\n### ANSI Color Mapping\n\n```dart\nconst colorMap = {\n  0: Color(0x000000),  // Black\n  1: Color(0x800000),  // Red\n  2: Color(0x008000),  // Green\n  3: Color(0x808000),  // Yellow\n  4: Color(0x000080),  // Blue\n  5: Color(0x800080),  // Magenta\n  6: Color(0x008080),  // Cyan\n  7: Color(0xC0C0C0),  // White\n  // ... 256-color palette\n};\n```\n\n### Theme Support\n\n- **Light**: Light background, dark text\n- **Dark**: Dark background, light text\n- **AMOLED**: Pure black background\n\n## Performance Optimizations\n\n### Rendering Optimizations\n\n- **Dirty rectangle**: Only redraw changed regions\n- **Line caching**: Cache rendered lines\n- **Lazy scrolling**: Virtual scrolling for long buffers\n\n### Data Optimizations\n\n- **Batch updates**: Coalesce multiple writes\n- **Compression**: Compress scroll buffer\n- **Debouncing**: Debounce rapid inputs\n\n## Clipboard Integration\n\n### Copy Selection\n\n```dart\nvoid copySelection() {\n  final selected = terminal.getSelection();\n  Clipboard.setData(ClipboardData(text: selected));\n}\n```\n\n### Paste Clipboard\n\n```dart\nFuture<void> pasteClipboard() async {\n  final data = await Clipboard.getData('text/plain');\n  if (data?.text != null) {\n    terminal.paste(data!.text!);\n  }\n}\n```\n\n### Context-Aware Button\n\n- **Has selection**: Show \"Copy\"\n- **Has clipboard**: Show \"Paste\"\n- **Both**: Show primary action\n\n## Special Features\n\n### Snippet Execution\n\n```dart\nvoid executeSnippet(Snippet snippet) {\n  final formatted = formatSnippet(snippet);\n  terminal.paste(formatted);\n  terminal.paste('\\r');  // Execute\n}\n```\n\n### SFTP Quick Access\n\n```dart\nvoid openSftp() async {\n  final cwd = await terminal.getCurrentWorkingDirectory();\n  Navigator.push(\n    context,\n    SftpPage(initialPath: cwd),\n  );\n}\n```\n\n### Keep-Alive\n\n```dart\nTimer.periodic(Duration(seconds: 30), (_) {\n  if (terminal.isActive) {\n    terminal.send('\\x00');  // NUL - no-op keep-alive\n  }\n});\n```\n"
  },
  {
    "path": "docs/src/content/docs/quick-start.mdx",
    "content": "---\ntitle: Quick Start\ndescription: Get up and running with Server Box in minutes\n---\n\nFollow this quick start guide to connect to your first server and start monitoring.\n\n## Step 1: Add a Server\n\n1. Open Server Box\n2. Tap the **+** button to add a new server\n3. Fill in the server information:\n   - **Name**: A friendly name for your server\n   - **Host**: IP address or domain name\n   - **Port**: SSH port (default: 22)\n   - **User**: SSH username\n   - **Password or Key**: Authentication method\n\n4. Tap **Save** to add the server\n\n## Step 2: Connect and Monitor\n\n1. Tap on your server card to connect\n2. The app will establish an SSH connection\n3. You'll see real-time status for:\n   - CPU usage\n   - Memory (RAM) and Swap\n   - Disk usage\n   - Network speed\n\n## Step 3: Explore Features\n\nOnce connected, you can:\n\n- **Open Terminal**: Tap the terminal button for full SSH access\n- **Browse Files**: Use SFTP to manage files\n- **Manage Containers**: View and control Docker containers\n- **View Processes**: Check running processes\n- **Run Snippets**: Execute saved commands\n\n## Tips\n\n- **Biometric Authentication**: Enable Face ID / Touch ID / Fingerprint for quick access (mobile)\n- **Home Screen Widgets**: Add server status widgets to your home screen (iOS/Android)\n- **Background Running**: Keep connections alive in the background (Android)\n"
  },
  {
    "path": "docs/src/content/docs/zh/advanced/bulk-import.md",
    "content": "---\ntitle: 批量导入服务器\ndescription: 从 JSON 文件中导入多个服务器\n---\n\n使用 JSON 文件一次性导入多个服务器配置。\n\n## JSON 格式\n\n:::danger[安全警告]\n**切勿在文件中存储明文密码！** 此 JSON 示例仅为了演示显示了密码字段，但你应该：\n\n- **优先使用 SSH 密钥** (`keyId`) 而不是 `pwd` - 它们更安全\n- 如果必须使用密码，请使用**密码管理器**或环境变量\n- 导入后**立即删除文件** - 不要让凭据散落在各处\n- **添加到 .gitignore** - 切勿将凭证文件提交到版本控制\n:::\n\n```json\n[\n  {\n    \"name\": \"My Server\",\n    \"ip\": \"example.com\",\n    \"port\": 22,\n    \"user\": \"root\",\n    \"pwd\": \"password\",\n    \"keyId\": \"\",\n    \"tags\": [\"production\"],\n    \"autoConnect\": false\n  }\n]\n```\n\n## 字段说明\n\n| 字段 | 必填 | 说明 |\n|-------|----------|-------------|\n| `name` | 是 | 显示名称 |\n| `ip` | 是 | 域名或 IP 地址 |\n| `port` | 是 | SSH 端口 (通常为 22) |\n| `user` | 是 | SSH 用户名 |\n| `pwd` | 否 | 密码 (不建议使用 - 请改用 SSH 密钥) |\n| `keyId` | 否 | SSH 密钥名称 (来自“私钥” - 推荐) |\n| `tags` | 否 | 组织标签 |\n| `autoConnect` | 否 | 启动时自动连接 |\n\n## 导入步骤\n\n1. 创建包含服务器配置的 JSON 文件\n2. 设置 → 备份 → 批量导入服务器\n3. 选择你的 JSON 文件\n4. 确认导入\n\n## 示例\n\n```json\n[\n  {\n    \"name\": \"Production\",\n    \"ip\": \"prod.example.com\",\n    \"port\": 22,\n    \"user\": \"admin\",\n    \"keyId\": \"my-key\",\n    \"tags\": [\"production\", \"web\"]\n  },\n  {\n    \"name\": \"Development\",\n    \"ip\": \"dev.example.com\",\n    \"port\": 2222,\n    \"user\": \"dev\",\n    \"keyId\": \"dev-key\",\n    \"tags\": [\"development\"]\n  }\n]\n```\n\n## 提示\n\n- 尽可能**使用 SSH 密钥**代替密码\n- 导入后**测试连接**\n- **使用标签组织**以便于管理\n- 导入后**删除 JSON 文件**\n- **切勿提交**包含凭据的 JSON 文件到版本控制\n"
  },
  {
    "path": "docs/src/content/docs/zh/advanced/custom-commands.md",
    "content": "---\ntitle: 自定义命令\ndescription: 在服务器页面上显示自定义命令输出\n---\n\n添加自定义 shell 命令，以在服务器详情页面上显示其输出。\n\n## 设置步骤\n\n1. 服务器设置 → 自定义命令\n2. 以 JSON 格式输入命令\n\n## 基础格式\n\n```json\n{\n  \"显示名称\": \"shell 命令\"\n}\n```\n\n**示例：**\n```json\n{\n  \"内存\": \"free -h\",\n  \"磁盘\": \"df -h\",\n  \"运行时间\": \"uptime\"\n}\n```\n\n## 查看结果\n\n设置完成后，自定义命令将显示在服务器详情页面上，并自动刷新。\n\n## 特殊命令名称\n\n### server_card_top_right\n\n显示在首页服务器卡片的右上角：\n\n```json\n{\n  \"server_card_top_right\": \"你的命令\"\n}\n```\n\n## 提示\n\n**使用绝对路径：**\n```json\n{\"我的脚本\": \"/usr/local/bin/my-script.sh\"}\n```\n\n**管道命令：**\n```json\n{\"占用最高进程\": \"ps aux | sort -rk 3 | head -5\"}\n```\n\n**格式化输出：**\n```json\n{\"CPU 负载\": \"uptime | awk -F'load average:' '{print $2}'\"}\n```\n\n**保持命令快速执行：** 最好在 5 秒内完成，以获得最佳体验。\n\n**限制输出内容：**\n```json\n{\"日志\": \"tail -20 /var/log/syslog\"}\n```\n\n## 安全性\n\n命令以 SSH 用户的权限运行。避免使用修改系统状态的命令。\n"
  },
  {
    "path": "docs/src/content/docs/zh/advanced/custom-logo.md",
    "content": "---\ntitle: 自定义服务器 Logo\ndescription: 为服务器卡片使用自定义图标\n---\n\n通过图片 URL 在服务器卡片上显示自定义 Logo。\n\n## 设置步骤\n\n1. 服务器设置 → 自定义 Logo\n2. 输入图片 URL\n\n## URL 占位符\n\n### {DIST} - Linux 发行版\n\n自动替换为检测到的发行版：\n\n```\nhttps://example.com/{DIST}.png\n```\n\n将变为：`debian.png`, `ubuntu.png`, `arch.png` 等。\n\n### {BRIGHT} - 主题\n\n自动替换为当前主题：\n\n```\nhttps://example.com/{BRIGHT}.png\n```\n\n将变为：`light.png` 或 `dark.png`。\n\n### 组合使用\n\n```\nhttps://example.com/{DIST}-{BRIGHT}.png\n```\n\n将变为：`debian-light.png`, `ubuntu-dark.png` 等。\n\n## 提示\n\n- 使用 PNG 或 SVG 格式\n- 建议尺寸：64x64 到 128x128 像素\n- 使用 HTTPS URL\n- 保持文件体积较小\n\n## 支持的发行版\n\ndebian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt\n\n完整列表：[`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)\n"
  },
  {
    "path": "docs/src/content/docs/zh/advanced/json-settings.md",
    "content": "---\ntitle: 隐藏设置 (JSON)\ndescription: 通过 JSON 编辑器访问高级设置\n---\n\n有些设置在 UI 中是隐藏的，但可以通过 JSON 编辑器进行访问。\n\n## 如何访问\n\n长按侧边栏中的**“设置”**即可打开 JSON 编辑器。\n\n## 常用隐藏设置\n\n### timeOut\n\n连接超时时间（秒）。\n\n```json\n{\"timeOut\": 10}\n```\n\n**类型：** 整数 | **默认值：** 5 | **范围：** 1-60\n\n### recordHistory\n\n保存历史记录（SFTP 路径等）。\n\n```json\n{\"recordHistory\": true}\n```\n\n**类型：** 布尔值 | **默认值：** true\n\n### textFactor\n\n文本缩放系数。\n\n```json\n{\"textFactor\": 1.2}\n```\n\n**类型：** 浮点数 | **默认值：** 1.0 | **范围：** 0.8-1.5\n\n## 查找更多设置\n\n所有设置都定义在 [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart) 中。\n\n查找如下代码：\n```dart\nlate final settingName = StoreProperty(box, 'settingKey', defaultValue);\n```\n\n## ⚠️ 重要提示\n\n**在编辑之前：**\n- **创建备份** - 错误的设置可能导致应用无法打开\n- **谨慎编辑** - JSON 必须有效\n\n## 恢复方法\n\n如果编辑后应用无法打开：\n1. 清除应用数据（最后手段）\n2. 重新安装应用\n3. 从备份恢复\n"
  },
  {
    "path": "docs/src/content/docs/zh/advanced/troubleshooting.md",
    "content": "---\ntitle: 常见问题\ndescription: 常见问题的解决方案\n---\n\n## 连接问题\n\n### SSH 无法连接\n\n**症状：** 超时、连接被拒绝、身份验证失败\n\n**解决方案：**\n\n1. **核对服务器类型：** 仅支持类 Unix 系统（Linux、macOS、Android/Termux）\n2. **手动测试：** 在终端运行 `ssh user@server -p port`\n3. **检查防火墙：** 确保 22 端口已开放\n4. **核对凭据：** 确保用户名、密码或密钥正确\n\n### 频繁掉线\n\n**症状：** 终端在闲置一段时间后自动断开\n\n**解决方案：**\n\n1. **服务器心跳设置：**\n   ```bash\n   # /etc/ssh/sshd_config\n   ClientAliveInterval 60\n   ClientAliveCountMax 3\n   ```\n\n2. **禁用电池优化：**\n   - MIUI：电池 → “不限制”\n   - Android：设置 → 应用 → 禁用电池优化\n   - iOS：开启后台刷新\n\n## 输入问题\n\n### 无法输入某些字符\n\n**解决方案：** 设置 → 键盘类型 → 切换为 `visiblePassword`\n\n注意：更改此项后，CJK（中日韩）输入法可能无法正常工作。\n\n## 应用问题\n\n### 应用在启动时崩溃\n\n**症状：** 应用打不开、黑屏\n\n**原因：** 设置损坏，特别是通过 JSON 编辑器修改后\n\n**解决方案：**\n\n1. **清除应用数据：**\n   - Android：设置 → 应用 → ServerBox → 清除数据\n   - iOS：删除并重新安装\n\n2. **恢复备份：** 导入更改设置前创建的备份文件\n\n### 备份/恢复失败\n\n**备份失败：**\n- 检查存储空间\n- 确保应用具有存储权限\n- 尝试不同的存储位置\n\n**恢复失败：**\n- 检查备份文件的完整性\n- 检查应用版本兼容性\n\n## 桌面组件（Widget）问题\n\n### 组件不更新\n\n**iOS：**\n- 自动刷新最多可能需要等待 30 分钟\n- 尝试删除并重新添加组件\n- 检查 URL 是否以 `/status` 结尾\n\n**Android：**\n- 点击组件以强制刷新\n- 确保组件 ID 与应用设置中的配置一致\n\n**watchOS：**\n- 重启手表应用\n- 更改配置后等待几分钟\n- 核对 URL 格式\n\n### 组件显示错误\n\n- 确保服务器上正在运行 ServerBox Monitor\n- 在浏览器中测试 URL\n- 检查身份验证凭据\n\n## 性能问题\n\n### 应用响应慢\n\n**解决方案：**\n- 在设置中降低刷新频率\n- 检查网络速度\n- 禁用不常用的服务器\n\n### 耗电量高\n\n**解决方案：**\n- 增加刷新间隔\n- 禁用后台刷新\n- 关闭不使用的 SSH 会话\n\n## 获取帮助\n\n如果问题仍然存在：\n\n1. **搜索 GitHub Issues：** https://github.com/lollipopkit/flutter_server_box/issues\n2. **提交新 Issue：** 请包含应用版本、平台和复现步骤\n3. **查看 Wiki：** 本文档以及 GitHub Wiki\n"
  },
  {
    "path": "docs/src/content/docs/zh/advanced/widgets.md",
    "content": "---\ntitle: 主屏幕小组件\ndescription: 在主屏幕上添加服务器状态小组件\n---\n\n需要在服务器上安装 [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor)。\n\n## 前置条件\n\n请先在你的服务器上安装 ServerBox Monitor。安装说明请参考 [ServerBox Monitor Wiki](https://github.com/lollipopkit/server_box_monitor/wiki/Home)。\n\n安装完成后，你的服务器应具备：\n- HTTP/HTTPS 端点\n- `/status` API 接口\n- 可选的身份验证\n\n## URL 格式\n\n```\nhttps://your-server.com/status\n```\n\n必须以 `/status` 结尾。\n\n## iOS 小组件\n\n### 设置步骤\n\n1. 长按主屏幕 → 点击 **+**\n2. 搜索 “ServerBox”\n3. 选择小组件尺寸\n4. 长按小组件 → **编辑小组件**\n5. 输入以 `/status` 结尾的 URL\n\n### 注意事项\n\n- 必须使用 HTTPS（局域网 IP 除外）\n- 最大刷新频率：30 分钟（iOS 系统限制）\n- 可以为多个服务器添加多个小组件\n\n## Android 小组件\n\n### 设置步骤\n\n1. 长按主屏幕 → **小组件**\n2. 找到 “ServerBox” → 添加到主屏幕\n3. 记下显示的 Widget ID 数字\n4. 打开 ServerBox 应用 → 设置\n5. 点击 **配置桌面小组件链接**\n6. 添加条目：`Widget ID` = `状态 URL`\n\n示例：\n- 键 (Key)：`17`\n- 值 (Value)：`https://my-server.com/status`\n\n7. 点击主屏幕上的小组件进行刷新\n\n## watchOS 小组件\n\n### 设置步骤\n\n1. 打开 iPhone 上的应用 → 设置\n2. **iOS 设置** → **Watch 应用**\n3. 点击 **添加 URL**\n4. 输入以 `/status` 结尾的 URL\n5. 等待手表端同步\n\n### 注意事项\n\n- 如果未更新，请尝试重启手表应用\n- 确保手机和手表已连接\n\n## 故障排除\n\n### 小组件不更新\n\n**iOS：** 等待最多 30 分钟，然后尝试删除并重新添加。\n**Android：** 点击小组件强制刷新，检查设置中的 ID 是否正确。\n**watchOS：** 重启手表应用，等待几分钟。\n\n### 小组件显示错误\n\n- 确保 ServerBox Monitor 正在运行\n- 在浏览器中测试 URL 是否可用\n- 检查 URL 是否以 `/status` 结尾\n\n## 安全性\n\n- 尽可能**始终使用 HTTPS**\n- **局域网 IP** 仅建议在信任的网络中使用\n"
  },
  {
    "path": "docs/src/content/docs/zh/development/architecture.md",
    "content": "---\ntitle: 架构\ndescription: 架构模式与设计决策\n---\n\nServer Box 遵循整洁架构 (Clean Architecture) 原则，在数据层、领域层和表现层之间实现了清晰的分离。\n\n## 分层架构\n\n```\n┌─────────────────────────────────────┐\n│          表现层 (Presentation)      │\n│         (lib/view/page/)            │\n│  - 页面、组件、控制器                │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│         业务逻辑层 (Business Logic) │\n│      (lib/data/provider/)           │\n│  - Riverpod Provider                │\n│  - 状态管理                         │\n└─────────────────────────────────────┘\n                 ↓\n┌─────────────────────────────────────┐\n│           数据层 (Data)             │\n│      (lib/data/model/, store/)      │\n│  - 模型、存储、服务                  │\n└─────────────────────────────────────┘\n```\n\n## 关键模式\n\n### 状态管理：Riverpod\n\n- **代码生成**：使用 `riverpod_generator` 实现类型安全的 provider\n- **State Notifiers**：用于包含业务逻辑的可变状态\n- **Async Notifiers**：用于处理加载中和错误状态\n- **Stream Providers**：用于处理实时数据\n\n### 不可变模型：Freezed\n\n- 所有数据模型均使用 Freezed 实现不可变性\n- 使用联合类型 (Union types) 表示不同状态\n- 内置 JSON 序列化支持\n- 提供 CopyWith 扩展以便于更新\n\n### 本地存储：Hive\n\n- **hive_ce**：Hive 的社区版\n- 无需手动添加 `@HiveField` 或 `@HiveType` 注解\n- 类型适配器 (Type adapters) 自动生成\n- 持久化键值对存储\n\n## 依赖注入 (DI)\n\n服务和存储类通过以下方式注入：\n\n1. **Provider**：向 UI 层暴露依赖\n2. **GetIt**：服务定位器（在适用情况下使用）\n3. **构造函数注入**：显式声明依赖关系\n\n## 数据流\n\n```\n用户操作 → Widget → Provider → 服务/存储 → 模型更新 → UI 重构\n```\n\n1. 用户与组件交互\n2. 组件调用 provider 方法\n3. Provider 通过服务/存储更新状态\n4. 状态更改触发 UI 重新构建\n5. 组件反映最新状态\n\n## 自定义依赖\n\n项目使用了多个自定义分支以扩展功能：\n\n- **dartssh2**：增强的 SSH 功能\n- **xterm**：支持移动端的终端模拟器\n- **fl_lib**：共享的 UI 组件和工具类\n\n## 多线程处理\n\n- **Isolates**：将繁重的计算任务移出主线程\n- **computer 包**：多线程工具类\n- **Async/Await**：非阻塞式 I/O 操作\n"
  },
  {
    "path": "docs/src/content/docs/zh/development/building.md",
    "content": "---\ntitle: 构建指南\ndescription: 不同平台的构建说明\n---\n\nServer Box 使用自定义构建系统 (`fl_build`) 进行跨平台构建。\n\n## 前置条件\n\n- Flutter SDK (stable channel)\n- 平台特定工具 (iOS 需要 Xcode，Android 需要 Android Studio)\n- Rust 工具链 (部分原生依赖项需要)\n\n## 开发版构建\n\n```bash\n# 以开发模式运行\nflutter run\n\n# 在指定设备上运行\nflutter run -d <device-id>\n```\n\n## 生产版构建\n\n项目使用 `fl_build` 进行构建：\n\n```bash\n# 为指定平台构建\ndart run fl_build -p <platform>\n\n# 可用平台：\n# - ios\n# - android\n# - macos\n# - linux\n# - windows\n```\n\n## 平台特定构建\n\n### iOS\n\n```bash\ndart run fl_build -p ios\n```\n\n需要：\n- 安装了 Xcode 的 macOS\n- CocoaPods\n- 用于签名的 Apple Developer 账号\n\n### Android\n\n```bash\ndart run fl_build -p android\n```\n\n需要：\n- Android SDK\n- Java Development Kit (JDK)\n- 用于签名的 Keystore\n\n### macOS\n\n```bash\ndart run fl_build -p macos\n```\n\n### Linux\n\n```bash\ndart run fl_build -p linux\n```\n\n### Windows\n\n```bash\ndart run fl_build -p windows\n```\n\n需要安装了 Visual Studio 的 Windows 环境。\n\n## 构建前/后处理\n\n`make.dart` 脚本负责处理：\n\n- 元数据生成\n- 版本字符串更新\n- 平台特定的配置\n\n## 故障排除\n\n### 全新构建 (Clean Build)\n\n```bash\nflutter clean\ndart run build_runner build --delete-conflicting-outputs\nflutter pub get\n```\n\n### 版本不匹配\n\n确保所有依赖项兼容：\n```bash\nflutter pub upgrade\n```\n\n## 发布清单\n\n1. 更新 `pubspec.yaml` 中的版本号\n2. 运行代码生成\n3. 运行测试\n4. 为所有目标平台构建\n5. 在真机上测试\n6. 创建 GitHub Release\n"
  },
  {
    "path": "docs/src/content/docs/zh/development/codegen.md",
    "content": "---\ntitle: 代码生成\ndescription: 使用 build_runner 进行代码生成\n---\n\nServer Box 大量使用代码生成技术来处理模型、状态管理和序列化。\n\n## 何时运行代码生成\n\n在修改以下内容后需要运行：\n\n- 带有 `@freezed` 注解的模型\n- 带有 `@JsonSerializable` 的类\n- Hive 模型\n- 带有 `@riverpod` 的 Provider\n- 本地化文件 (ARB 文件)\n\n## 运行代码生成\n\n```bash\n# 生成所有代码\ndart run build_runner build --delete-conflicting-outputs\n\n# 清理并重新生成\ndart run build_runner build --delete-conflicting-outputs --clean\n```\n\n## 生成的文件类型\n\n### Freezed (`*.freezed.dart`)\n\n具有联合类型 (Union types) 的不可变数据模型：\n\n```dart\n@freezed\nclass ServerState with _$ServerState {\n  const factory ServerState.connected() = Connected;\n  const factory ServerState.disconnected() = Disconnected;\n  const factory ServerState.error(String message) = Error;\n}\n```\n\n### JSON 序列化 (`*.g.dart`)\n\n由 `json_serializable` 生成：\n\n```dart\n@JsonSerializable()\nclass Server {\n  final String id;\n  final String name;\n  final String host;\n\n  Server({required this.id, required this.name, required this.host});\n\n  factory Server.fromJson(Map<String, dynamic> json) =>\n      _$ServerFromJson(json);\n  Map<String, dynamic> toJson() => _$ServerToJson(this);\n}\n```\n\n### Riverpod Provider (`*.g.dart`)\n\n由 `@riverpod` 注解生成：\n\n```dart\n@riverpod\nclass MyNotifier extends _$MyNotifier {\n  @override\n  int build() => 0;\n}\n```\n\n### Hive 适配器 (`*.g.dart`)\n\n为 Hive 模型 (hive_ce) 自动生成：\n\n```dart\n@HiveType(typeId: 0)\nclass ServerModel {\n  @HiveField(0)\n  final String id;\n}\n```\n\n## 生成本地化代码\n\n```bash\nflutter gen-l10n\n```\n\n根据 `lib/l10n/*.arb` 文件生成 `lib/generated/l10n/` 目录下的代码。\n\n## 提示\n\n- 使用 `--delete-conflicting-outputs` 避免冲突\n- 将生成的文件添加到 `.gitignore`\n- **切勿**手动编辑生成的文件\n"
  },
  {
    "path": "docs/src/content/docs/zh/development/state.md",
    "content": "---\ntitle: 状态管理\ndescription: 基于 Riverpod 的状态管理模式\n---\n\nServer Box 使用 Riverpod 及其代码生成工具进行状态管理。\n\n## Provider 类型\n\n### StateProvider\n\n可读写的简单状态：\n\n```dart\n@riverpod\nclass Settings extends _$Settings {\n  @override\n  SettingsModel build() {\n    return SettingsModel.defaults();\n  }\n\n  void update(SettingsModel newSettings) {\n    state = newSettings;\n  }\n}\n```\n\n### AsyncNotifierProvider\n\n具有加载中/错误状态的异步加载状态：\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    return fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() => fetchStatus(server));\n  }\n}\n```\n\n### StreamProvider\n\n来自数据流的实时数据：\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  return cpuService.monitor(server);\n}\n```\n\n## 状态模式\n\n### 加载状态处理\n\n```dart\nstate.when(\n  data: (data) => DataWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### Family Provider (带参数的 Provider)\n\n带有参数的 Provider：\n\n```dart\n@riverpod\nList<Container> containers(ContainersRef ref, Server server) {\n  return containerService.list(server);\n}\n```\n\n### 自动释放 (Auto-Dispose)\n\n当不再被引用时自动销毁的 Provider：\n\n```dart\n@Riverpod(keepAlive: false)\nclass TempState extends _$TempState {\n  // ...\n}\n```\n\n## 最佳实践\n\n1. **使用代码生成**：始终使用 `@riverpod` 注解。\n2. **就近放置 Provider**：将 Provider 定义在消费它的 Widget 附近。\n3. **避免使用单例**：改用 Provider。\n4. **正确的分层**：保持 UI 逻辑与业务逻辑的分离。\n\n## 在 Widget 中读取状态\n\n```dart\nclass ServerWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final status = ref.watch(serverStatusProvider(server));\n    return status.when(...);\n  }\n}\n```\n\n## 修改状态\n\n```dart\nref.read(settingsProvider.notifier).update(newSettings);\n```\n"
  },
  {
    "path": "docs/src/content/docs/zh/development/structure.md",
    "content": "---\ntitle: 项目结构\ndescription: 了解 Server Box 的代码库结构\n---\n\nServer Box 项目遵循模块化架构，具有清晰的关注点分离。\n\n## 目录结构\n\n```\nlib/\n├── core/              # 核心工具类和扩展\n├── data/              # 数据层\n│   ├── model/         # 按功能划分的数据模型\n│   ├── provider/      # Riverpod provider\n│   └── store/         # 本地存储 (Hive)\n├── view/              # UI 层\n│   ├── page/          # 主要页面\n│   └── widget/        # 可复用组件\n├── generated/         # 生成的本地化代码\n├── l10n/              # 本地化 ARB 文件\n└── hive/              # Hive 适配器\n```\n\n## 核心层 (`lib/core/`)\n\n包含工具类、扩展和路由配置：\n\n- **Extensions**：针对通用类型的 Dart 扩展\n- **Routes**：应用路由配置\n- **Utils**：共享的工具函数\n\n## 数据层 (`lib/data/`)\n\n### 模型 (`lib/data/model/`)\n\n按功能模块组织：\n\n- `server/` - 服务器连接及状态模型\n- `container/` - Docker 容器模型\n- `ssh/` - SSH 会话模型\n- `sftp/` - SFTP 文件模型\n- `app/` - 应用特定的模型\n\n### Provider (`lib/data/provider/`)\n\n用于依赖注入和状态管理的 Riverpod provider：\n\n- 服务器 Provider\n- UI 状态 Provider\n- 服务 Provider\n\n### 存储 (`lib/data/store/`)\n\n基于 Hive 的本地存储：\n\n- 服务器存储\n- 设置存储\n- 缓存存储\n\n## 视图层 (`lib/view/`)\n\n### 页面 (`lib/view/page/`)\n\n应用程序的主要屏幕：\n\n- `server/` - 服务器管理页面\n- `ssh/` - SSH 终端页面\n- `container/` - 容器管理页面\n- `setting/` - 设置页面\n- `storage/` - SFTP 页面\n- `snippet/` - 脚本页面\n\n### 组件 (`lib/view/widget/`)\n\n可复用的 UI 组件：\n\n- 服务器卡片\n- 状态图表\n- 输入组件\n- 对话框\n\n## 生成的文件\n\n- `lib/generated/l10n/` - 自动生成的本地化代码\n- `*.g.dart` - 生成的代码 (json_serializable, freezed, hive, riverpod)\n- `*.freezed.dart` - Freezed 不可变类\n\n## Packages 目录 (`/packages/`)\n\n包含依赖项的自定义分支：\n\n- `dartssh2/` - SSH 库\n- `xterm/` - 终端模拟器\n- `fl_lib/` - 共享工具类\n- `fl_build/` - 构建系统\n"
  },
  {
    "path": "docs/src/content/docs/zh/development/testing.md",
    "content": "---\ntitle: 测试指南\ndescription: 测试策略与运行测试\n---\n\n## 运行测试\n\n```bash\n# 运行所有测试\nflutter test\n\n# 运行特定测试文件\nflutter test test/battery_test.dart\n\n# 运行测试并生成覆盖率报告\nflutter test --coverage\n```\n\n## 测试结构\n\n测试文件位于 `test/` 目录中，其结构与 `lib` 目录保持一致：\n\n```\ntest/\n├── data/\n│   ├── model/\n│   └── provider/\n├── view/\n│   └── widget/\n└── test_helpers.dart\n```\n\n## 单元测试\n\n测试业务逻辑和数据模型：\n\n```dart\ntest('应当计算 CPU 百分比', () {\n  final cpu = CpuModel(usage: 75.0);\n  expect(cpu.usagePercentage, '75%');\n});\n```\n\n## Widget 测试\n\n测试 UI 组件：\n\n```dart\ntestWidgets('ServerCard 应当显示服务器名称', (tester) async {\n  await tester.pumpWidget(\n    ProviderScope(\n      child: MaterialApp(\n        home: ServerCard(server: testServer),\n      ),\n    ),\n  );\n\n  expect(find.text('Test Server'), findsOneWidget);\n});\n```\n\n## Provider 测试\n\n测试 Riverpod provider：\n\n```dart\ntest('serverStatusProvider 应当返回状态', () async {\n  final container = ProviderContainer();\n  final status = await container.read(serverStatusProvider(testServer).future);\n  expect(status, isA<StatusModel>());\n});\n```\n\n## Mock 模拟\n\n对外部依赖使用 Mock 模拟：\n\n```dart\nclass MockSshService extends Mock implements SshService {}\n\ntest('应当能连接到服务器', () async {\n  final mockSsh = MockSshService();\n  when(mockSsh.connect(any)).thenAnswer((_) async => true);\n\n  // 使用 mock 进行测试\n});\n```\n\n## 集成测试\n\n测试完整的用户流程（位于 `integration_test/`）：\n\n```dart\ntestWidgets('添加服务器流程', (tester) async {\n  await tester.pumpWidget(MyApp());\n\n  // 点击添加按钮\n  await tester.tap(find.byIcon(Icons.add));\n  await tester.pumpAndSettle();\n\n  // 填写表单\n  await tester.enterText(find.byKey(Key('name')), 'Test Server');\n  // ...\n});\n```\n\n## 最佳实践\n\n1. **Arrange-Act-Assert**：清晰地组织测试结构（准备-执行-断言）\n2. **描述性名称**：测试名称应描述其行为\n3. **每个测试仅一个断言**：保持测试的专注度\n4. **Mock 外部依赖**：不要依赖真实服务器\n5. **测试边缘情况**：处理空列表、空值等\n"
  },
  {
    "path": "docs/src/content/docs/zh/index.mdx",
    "content": "---\ntitle: Server Box\ndescription: 一款全面的跨平台服务器管理应用\nhero:\n  tagline: 随时随地管理您的 Linux 服务器\n  actions:\n    - text: 开始使用\n      link: /zh/introduction/\n      icon: right-arrow\n      variant: primary\n    - text: 在 GitHub 上查看\n      link: https://github.com/lollipopkit/flutter_server_box\n      icon: github\n      variant: minimal\n---\n\nimport { Card, CardGrid } from '@astrojs/starlight/components';\n\n## 特性\n\n<CardGrid stagger>\n\t<Card title=\"实时监控\" icon=\"chart\">\n\t\t通过精美的实时图表监控 CPU、内存、磁盘、网络、GPU 和温度。\n\t</Card>\n\t<Card title=\"SSH 终端\" icon=\"terminal\">\n\t\t全功能 SSH 终端，支持多标签页和移动端虚拟键盘。\n\t</Card>\n\t<Card title=\"SFTP 文件浏览器\" icon=\"folder\">\n\t\t使用内置 SFTP 客户端和本地文件浏览器管理服务器上的文件。\n\t</Card>\n\t<Card title=\"容器管理\" icon=\"box\">\n\t\t通过直观的界面启动、停止和监控 Docker 和 Podman 容器。\n\t</Card>\n\t<Card title=\"跨平台\" icon=\"device-mobile\">\n\t\t支持 iOS、Android、macOS、Linux、Windows 和 watchOS。\n\t</Card>\n\t<Card title=\"12+ 种语言\" icon=\"globe\">\n\t\t完善的本地化支持，包括英语、中文、德语、法语等。\n\t</Card>\n</CardGrid>\n\n## 快速链接\n\n- **下载**: 可在 [App Store](https://apps.apple.com/app/id1586449703), [GitHub](https://github.com/lollipopkit/flutter_server_box/releases), [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox), [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) 和 [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) 获取\n- **文档**: 探索指南以开始使用 Server Box\n- **支持**: 加入我们的 GitHub 社区进行讨论和问题反馈\n"
  },
  {
    "path": "docs/src/content/docs/zh/installation.mdx",
    "content": "---\ntitle: 安装\ndescription: 在您的设备上下载并安装 Server Box\n---\n\nServer Box 适用于多个平台。选择您偏好的安装方式。\n\n## 移动应用\n\n### iOS\n\n从 **[App Store](https://apps.apple.com/app/id1586449703)** 下载。\n\n### Android\n\n选择您偏好的来源：\n\n- **[F-Droid](https://f-droid.org/packages/tech.lolli.toolbox)** - 适合偏好 FOSS 来源的用户\n- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** - 直接从源获取最新版本\n- **[CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid)** - CDN\n- **[OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)** - 第三方应用商店\n\n## 桌面应用\n\n### macOS\n\n从 **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** 下载。\n\n特性：\n- 原生菜单栏集成\n- 支持 Intel 和 Apple Silicon\n\n### Linux\n\n从 **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** 下载。\n\n提供 AppImage、deb 或 tar.gz 格式包。\n\n### Windows\n\n从 **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** 下载。\n\n## watchOS\n\n作为 iOS 应用的一部分，可在 **[App Store](https://apps.apple.com/app/id1586449703)** 上获取。\n\n## 从源码构建\n\n要从源码构建 Server Box，请参阅开发文档中的[构建](/zh/development/building)部分。\n\n## 版本信息\n\n查看 [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) 页面以获取最新版本和变更日志。\n"
  },
  {
    "path": "docs/src/content/docs/zh/introduction.mdx",
    "content": "---\ntitle: 介绍\ndescription: 了解 Server Box 是什么以及它能做什么\n---\n\nServer Box 是一款使用 Flutter 构建的全面跨平台服务器管理应用。它允许您随时随地监控、管理和控制您的 Linux、Unix 和 Windows 服务器。\n\n## 什么是 Server Box?\n\nServer Box 通过 SSH 连接为服务器管理任务提供统一的界面。无论您是系统管理员、开发人员，还是运行家庭服务器的爱好者，此应用都能为您提供强大的服务器管理工具。\n\n## 核心功能\n\n- **实时监控**：追踪 CPU、内存、磁盘使用率、网络速度、GPU 状态和系统温度\n- **SSH 终端**：全功能终端访问，支持多标签页和可自定义外观\n- **SFTP 客户端**：浏览和管理服务器上的文件\n- **容器管理**：轻松控制 Docker 和 Podman 容器\n- **进程管理**：查看和管理系统进程\n- **Systemd 服务**：启动、停止和监控 systemd 服务\n- **网络工具**：iPerf 测试、Ping 和网络唤醒 (WoL)\n- **代码片段**：保存并执行自定义 Shell 命令\n\n## 支持平台\n\nServer Box 是真正的跨平台应用：\n\n- **移动端**：iOS 和 Android\n- **桌面端**：macOS、Linux 和 Windows\n\n## 许可证\n\n本项目采用 AGPL v3 许可证。源代码可在 [GitHub](https://github.com/lollipopkit/flutter_server_box) 上获得。\n"
  },
  {
    "path": "docs/src/content/docs/zh/platforms/desktop.md",
    "content": "---\ntitle: 桌面端功能\ndescription: macOS, Linux 和 Windows 的平台特定功能\n---\n\nServer Box 在桌面平台上提供了额外的效率功能。\n\n## macOS\n\n### 菜单栏集成\n\n- 在菜单栏快速查看服务器状态\n- 一键访问服务器\n- 精简模式，减少干扰\n- 原生 macOS 菜单栏样式\n\n### 窗口状态持久化\n\n- 记忆窗口位置和大小\n- 启动时恢复上一次会话\n- 支持多显示器\n\n### 原生特性\n\n- **标题栏**：可选自定义或系统标题栏\n- **全屏模式**：专用的服务器监控视图\n- **键盘快捷键**：macOS 原生快捷键\n- **Touch Bar**（受支持的设备）：快速操作\n\n## Linux\n\n### 原生集成\n\n- 支持系统托盘\n- 桌面通知集成\n- 文件选择器集成\n\n### 窗口管理\n\n- 支持 X11 和 Wayland\n- 对平铺窗口管理器友好\n- 可选自定义窗口装饰\n\n## Windows\n\n### 功能特性\n\n- 系统托盘集成\n- 跳转列表 (Jump List) 快速操作\n- 原生窗口控制\n- 开机自启选项\n\n## 跨平台桌面功能\n\n### 键盘快捷键\n\n- **Cmd/Ctrl + ,**：打开设置\n- **Cmd/Ctrl + Q**：退出应用（仅 macOS）\n- **Cmd/Ctrl + 1-9**：切换标签页\n\n### 主题\n\n- 浅色模式\n- 深色模式\n- AMOLED 模式（纯黑）\n- 自动 AMOLED 模式（根据系统设置自动切换）\n- 系统模式（跟随系统设置）\n\n### 桌面端优势\n\n- 更大的屏幕用于监控\n- 全尺寸键盘更适合终端操作\n- 更快的文件操作速度\n- 更好的多任务处理\n"
  },
  {
    "path": "docs/src/content/docs/zh/platforms/mobile.md",
    "content": "---\ntitle: 移动端功能\ndescription: iOS 和 Android 的平台特定功能\n---\n\nServer Box 为 iOS 和 Android 设备提供了多项移动端特有功能。\n\n## 生物识别身份验证\n\n使用生物识别技术保护你的服务器安全：\n\n- **iOS**：Face ID 或 Touch ID\n- **Android**：指纹识别\n\n在“设置 > 安全 > 生物识别身份验证”中启用。\n\n## 主屏幕小组件\n\n在主屏幕添加服务器状态小组件，实现快速监控。\n\n### iOS\n\n- 长按主屏幕\n- 点击 **+** 添加小组件\n- 搜索 “Server Box”\n- 选择小组件尺寸：\n  - 小号：单个服务器状态\n  - 中号：多个服务器\n  - 大号：详细信息\n\n### Android\n\n- 长按主屏幕\n- 点击**小组件**\n- 找到 “Server Box”\n- 选择小组件类型\n\n## 后台运行\n\n### Android\n\n在后台保持连接活跃：\n\n- 在“设置 > 高级 > 后台运行”中启用\n- 需要排除电池优化\n- 活动连接将显示持久通知\n\n### iOS\n\n受限于后台限制：\n\n- 后台连接可能会暂停\n- 返回应用时快速重连\n- 支持后台刷新\n\n## 推送通知\n\n接收以下通知：\n\n- 服务器离线警报\n- 资源占用过高警告\n- 任务完成提醒\n\n在“设置 > 通知”中配置。\n\n## 移动端 UI 特性\n\n- **下拉刷新**：更新服务器状态\n- **滑动操作**：快速进行服务器操作\n- **横屏模式**：提供更佳的终端体验\n- **虚拟键盘**：提供终端常用快捷键\n\n## 文件集成\n\n- **文件应用 (iOS)**：通过“文件”应用直接访问 SFTP\n- **存储访问框架 (Android)**：与其他应用共享文件\n- **文档选择器**：便捷的文件选择体验\n"
  },
  {
    "path": "docs/src/content/docs/zh/principles/architecture.md",
    "content": "---\ntitle: 架构概览\ndescription: 应用程序的高层架构设计\n---\n\nServer Box 采用分层架构，实现了清晰的关注点分离。\n\n## 架构分层\n\n```\n┌─────────────────────────────────────────────────┐\n│          表现层 (UI)                            │\n│          lib/view/page/, lib/view/widget/       │\n│  - 页面、组件、控制器                            │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│         业务逻辑层                              │\n│         lib/data/provider/                      │\n│  - Riverpod Provider, State Notifier            │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│           数据访问层                            │\n│         lib/data/store/, lib/data/model/        │\n│  - Hive 存储, 数据模型                          │\n└─────────────────────────────────────────────────┘\n                      ↓\n┌─────────────────────────────────────────────────┐\n│         外部集成层                              │\n│  - SSH (dartssh2), 终端 (xterm), SFTP           │\n│  - 平台特定代码 (iOS, Android 等)               │\n└─────────────────────────────────────────────────┘\n```\n\n## 应用基础\n\n### 入口点\n\n`lib/main.dart` 初始化应用：\n\n```dart\nvoid main() {\n  runApp(\n    ProviderScope(\n      child: MyApp(),\n    ),\n  );\n}\n```\n\n### 根组件\n\n`MyApp` 提供以下功能：\n- **主题管理**：浅色/深色主题切换\n- **路由配置**：导航结构\n- **Provider Scope**：依赖注入根节点\n\n### 首页\n\n`HomePage` 作为导航枢纽：\n- **标签页界面**：服务器、脚本、容器、SSH\n- **状态管理**：各标签页独立状态\n- **导航**：功能入口\n\n## 核心系统\n\n### 状态管理：Riverpod\n\n**为何选择 Riverpod？**\n- 编译时安全\n- 易于测试\n- 不依赖 Build context\n- 跨平台兼容性好\n\n**使用的 Provider 类型：**\n- `StateProvider`：简单的可变状态\n- `AsyncNotifierProvider`：处理加载/错误/数据状态\n- `StreamProvider`：实时数据流\n- Future providers：一次性异步操作\n\n### 数据持久化：Hive CE\n\n**为何选择 Hive CE？**\n- 无原生代码依赖\n- 快速的键值存储\n- 通过代码生成实现类型安全\n- 无需手动添加字段注解\n\n**存储类：**\n- `SettingStore`：应用偏好设置\n- `ServerStore`：服务器配置\n- `SnippetStore`：命令脚本\n- `KeyStore`：SSH 密钥\n\n### 不可变模型：Freezed\n\n**优势：**\n- 编译时不可变性\n- 联合类型处理状态\n- 内置 JSON 序列化\n- CopyWith 扩展\n\n## 跨平台策略\n\n### 插件系统\n\nFlutter 插件提供平台集成：\n\n| 平台 | 集成方式 |\n|----------|-------------------|\n| iOS | CocoaPods, Swift/Obj-C |\n| Android | Gradle, Kotlin/Java |\n| macOS | CocoaPods, Swift |\n| Linux | CMake, C++ |\n| Windows | CMake, C# |\n\n### 平台特定功能\n\n**仅限 iOS：**\n- 主屏幕小组件\n- 实时活动 (Live Activities)\n- Apple Watch 配套应用\n\n**仅限 Android：**\n- 后台服务\n- 推送通知\n- 文件系统访问\n\n**仅限桌面端：**\n- 菜单栏集成\n- 多窗口支持\n- 自定义标题栏\n\n## 自定义依赖\n\n### dartssh2 分支\n\n增强版 SSH 客户端，具有：\n- 更好的移动端支持\n- 增强的错误处理\n- 性能优化\n\n### xterm.dart 分支\n\n终端模拟器，具有：\n- 移动端优化的渲染\n- 手势支持\n- 虚拟键盘集成\n\n### fl_lib\n\n共享工具包，包含：\n- 通用组件\n- 扩展方法\n- 辅助函数\n\n## 构建系统\n\n### fl_build 包\n\n自定义构建系统，用于：\n- 多平台构建\n- 代码签名\n- 资源打包\n- 版本管理\n\n### 构建流程\n\n```\nmake.dart (版本计算) → fl_build (执行构建) → 平台产物\n```\n\n1. **预构建**：从 Git 计算版本号\n2. **构建**：为目标平台编译\n3. **后构建**：打包和签名\n\n## 数据流示例\n\n### 服务器状态更新\n\n```\n1. 定时器触发 →\n2. Provider 调用 service →\n3. Service 执行 SSH 命令 →\n4. 响应解析为模型 →\n5. 状态更新 →\n6. UI 使用新数据重新构建\n```\n\n### 用户操作流\n\n```\n1. 用户点击按钮 →\n2. Widget 调用 provider 方法 →\n3. Provider 更新状态 →\n4. 状态更改触发重构 →\n5. UI 反映新状态\n```\n\n## 安全架构\n\n### 数据保护\n\n- **密码**：使用 flutter_secure_storage 加密\n- **SSH 密钥**：静态存储时加密\n- **主机指纹**：安全存储\n- **会话数据**：不进行持久化\n\n### 连接安全\n\n- **主机密钥验证**：检测中间人攻击\n- **加密**：标准 SSH 加密\n- **不存储明文**：敏感数据绝不以明文存储\n"
  },
  {
    "path": "docs/src/content/docs/zh/principles/sftp.md",
    "content": "---\ntitle: SFTP 系统\ndescription: SFTP 文件浏览器的工作原理\n---\n\nSFTP 系统通过 SSH 提供文件管理功能。\n\n## 架构\n\n```\n┌─────────────────────────────────────────────┐\n│              SFTP UI 层                     │\n│  - 文件浏览器 (远程)                        │\n│  - 文件浏览器 (本地)                        │\n│  - 传输队列                                 │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          SFTP 状态管理                      │\n│  - sftpProvider                             │\n│  - 路径管理                                 │\n│  - 操作队列                                 │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         SFTP 协议层                         │\n│  - SSH 子系统                               │\n│  - 文件操作                                 │\n│  - 目录列表                                 │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│            SSH 传输层                       │\n│  - 安全通道                                 │\n│  - 数据流                                   │\n└─────────────────────────────────────────────┘\n```\n\n## 连接建立\n\n### 创建 SFTP 客户端\n\n```dart\nFuture<SftpClient> createSftpClient(Spi spi) async {\n  // 1. 获取 SSH 客户端 (如果可用则复用)\n  final sshClient = await genClient(spi);\n\n  // 2. 打开 SFTP 子系统\n  final sftp = await sshClient.openSftp();\n\n  return sftp;\n}\n```\n\n### 连接复用\n\nSFTP 复用现有的 SSH 连接：\n\n```dart\nclass ServerProvider {\n  SSHClient? _sshClient;\n  SftpClient? _sftpClient;\n\n  Future<SftpClient> getSftpClient(String spiId) async {\n    _sftpClient ??= await _sshClient!.openSftp();\n    return _sftpClient!;\n  }\n}\n```\n\n## 文件系统操作\n\n### 目录列表\n\n```dart\nFuture<List<SftpFile>> listDirectory(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 获取目录列表\n  final files = await sftp.listDir(path);\n\n  // 根据设置排序\n  files.sort((a, b) {\n    switch (sortOption) {\n      case SortOption.name:\n        return a.name.toLowerCase().compareTo(b.name.toLowerCase());\n      case SortOption.size:\n        return a.size.compareTo(b.size);\n      case SortOption.time:\n        return a.modified.compareTo(b.modified);\n    }\n  });\n\n  // 如果启用，文件夹优先\n  if (showFoldersFirst) {\n    final dirs = files.where((f) => f.isDirectory);\n    final regular = files.where((f) => !f.isDirectory);\n    return [...dirs, ...regular];\n  }\n\n  return files;\n}\n```\n\n### 文件元数据\n\n```dart\nclass SftpFile {\n  final String name;\n  final String path;\n  final int size;           // 字节\n  final int modified;       // Unix 时间戳\n  final String permissions;  // 例如 \"rwxr-xr-x\"\n  final String owner;\n  final String group;\n  final bool isDirectory;\n  final bool isSymlink;\n\n  String get sizeFormatted => formatBytes(size);\n  String get modifiedFormatted => formatDate(modified);\n}\n```\n\n## 文件操作\n\n### 上传\n\n```dart\nFuture<void> uploadFile(\n  String localPath,\n  String remotePath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 创建请求\n  final req = SftpReq(\n    spi: spi,\n    remotePath: remotePath,\n    localPath: localPath,\n    type: SftpReqType.upload,\n  );\n\n  // 添加到队列\n  _transferQueue.add(req);\n\n  // 执行带进度的传输\n  final file = File(localPath);\n  final size = await file.length();\n  final stream = file.openRead();\n\n  await sftp.upload(\n    stream: stream,\n    toPath: remotePath,\n    onProgress: (transferred) {\n      _updateProgress(req, transferred, size);\n    },\n  );\n\n  // 完成\n  _transferQueue.remove(req);\n}\n```\n\n### 下载\n\n```dart\nFuture<void> downloadFile(\n  String remotePath,\n  String localPath,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 创建本地文件\n  final file = File(localPath);\n  final sink = file.openWrite();\n\n  // 执行带进度的下载\n  final stat = await sftp.stat(remotePath);\n\n  await sftp.download(\n    fromPath: remotePath,\n    toSink: sink,\n    onProgress: (transferred) {\n      _updateProgress(\n        SftpReq(...),\n        transferred,\n        stat.size,\n      );\n    },\n  );\n\n  await sink.close();\n}\n```\n\n### 权限编辑\n\n```dart\nFuture<void> setPermissions(\n  String path,\n  String permissions,\n) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 解析权限 (例如 \"rwxr-xr-x\" 或 \"755\")\n  final mode = parsePermissions(permissions);\n\n  // 通过 SSH 命令设置 (比 SFTP 更可靠)\n  final ssh = await getSshClient(spiId);\n  await ssh.exec('chmod $mode \"$path\"');\n}\n```\n\n## 路径管理\n\n### 路径结构\n\n```dart\nclass PathWithPrefix {\n  final String prefix;  // 例如 \"/home/user\"\n  final String path;    // 相对或绝对路径\n\n  String get fullPath {\n    if (path.startsWith('/')) {\n      return path;  // 绝对路径\n    }\n    return '$prefix/$path';  // 相对路径\n  }\n\n  PathWithPrefix cd(String subPath) {\n    return PathWithPrefix(\n      prefix: fullPath,\n      path: subPath,\n    );\n  }\n}\n```\n\n### 导航历史\n\n```dart\nclass PathHistory {\n  final List<String> _history = [];\n  int _index = -1;\n\n  void push(String path) {\n    // 移除前进历史\n    _history.removeRange(_index + 1, _history.length);\n    _history.add(path);\n    _index = _history.length - 1;\n  }\n\n  String? back() {\n    if (_index > 0) {\n      _index--;\n      return _history[_index];\n    }\n    return null;\n  }\n\n  String? forward() {\n    if (_index < _history.length - 1) {\n      _index++;\n      return _history[_index];\n    }\n    return null;\n  }\n}\n```\n\n## 传输系统\n\n### 传输请求\n\n```dart\nclass SftpReq {\n  final Spi spi;\n  final String remotePath;\n  final String localPath;\n  final SftpReqType type;\n  final DateTime createdAt;\n\n  int? totalBytes;\n  int? transferredBytes;\n  String? error;\n}\n```\n\n### 进度跟踪\n\n```dart\nclass TransferProgress {\n  final SftpReq request;\n  final int total;\n  final int transferred;\n  final DateTime startTime;\n\n  double get percentage => (transferred / total) * 100;\n  Duration get elapsed => DateTime.now().difference(startTime);\n\n  String get speedFormatted {\n    final bytesPerSecond = transferred / elapsed.inSeconds;\n    return formatSpeed(bytesPerSecond);\n  }\n}\n```\n\n### 队列管理\n\n```dart\nclass TransferQueue {\n  final List<SftpReq> _queue = [];\n  final Map<String, TransferProgress> _progress = {};\n  int _concurrent = 3;  // 最大并发传输数\n\n  Future<void> process() async {\n    final active = _progress.values.where((p) => p.isInProgress);\n    if (active.length >= _concurrent) return;\n\n    final pending = _queue.where((r) => !_progress.containsKey(r.id));\n    for (final req in pending.take(_concurrent - active.length)) {\n      _executeTransfer(req);\n    }\n  }\n\n  Future<void> _executeTransfer(SftpReq req) async {\n    try {\n      _progress[req.id] = TransferProgress.inProgress(req);\n\n      if (req.type == SftpReqType.upload) {\n        await uploadFile(req.localPath, req.remotePath);\n      } else {\n        await downloadFile(req.remotePath, req.localPath);\n      }\n\n      _progress[req.id] = TransferProgress.completed(req);\n    } catch (e) {\n      _progress[req.id] = TransferProgress.failed(req, e);\n    }\n  }\n}\n```\n\n## 本地存储模式\n\n### 下载缓存\n\n下载的文件存储在：\n\n```dart\nString getLocalDownloadPath(String spiId, String remotePath) {\n  final normalized = remotePath.replaceAll('/', '_');\n  return 'Paths.file/$spiId/$normalized';\n}\n```\n\n示例：\n- 远程：`/var/log/nginx/access.log`\n- spiId：`server-123`\n- 本地：`Paths.file/server-123/_var_log_nginx_access.log`\n\n## 文件编辑\n\n### 编辑工作流\n\n```dart\nFuture<void> editFile(String path) async {\n  final sftp = await getSftpClient(spiId);\n\n  // 1. 检查大小\n  final stat = await sftp.stat(path);\n  if (stat.size > editorMaxSize) {\n    showWarning('文件太大，内置编辑器无法打开');\n    return;\n  }\n\n  // 2. 下载到临时目录\n  final temp = await downloadToTemp(path);\n\n  // 3. 在编辑器中打开\n  final content = await openEditor(temp.path);\n\n  // 4. 上传回服务器\n  await uploadFile(temp.path, path);\n\n  // 5. 清理\n  await temp.delete();\n}\n```\n\n### 外部编辑器集成\n\n```dart\nFuture<void> editInExternalEditor(String path) async {\n  final ssh = await getSshClient(spiId);\n\n  // 使用编辑器打开终端\n  final editor = getSetting('sftpEditor', 'vim');\n  await ssh.exec('$editor \"$path\"');\n\n  // 用户在终端中编辑\n  // 保存后，刷新 SFTP 视图\n}\n```\n\n## 错误处理\n\n### 权限错误\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpPermissionException {\n  showError('拒绝访问：${stat.path}');\n  showHint('请检查文件权限和所有权');\n}\n```\n\n### 连接错误\n\n```dart\ntry {\n  await sftp.listDir(path);\n} on SftpConnectionException {\n  showError('连接丢失');\n  await reconnect();\n}\n```\n\n### 空间错误\n\n```dart\ntry {\n  await sftp.upload(...);\n} on SftpNoSpaceException {\n  showError('远程服务器磁盘空间不足');\n}\n```\n\n## 性能优化\n\n### 目录缓存\n\n```dart\nclass DirectoryCache {\n  final Map<String, CachedDirectory> _cache = {};\n  final Duration ttl = Duration(minutes: 5);\n\n  Future<List<SftpFile>> list(String path) async {\n    final cached = _cache[path];\n    if (cached != null && !cached.isExpired) {\n      return cached.files;\n    }\n\n    final files = await sftp.listDir(path);\n    _cache[path] = CachedDirectory(files);\n    return files;\n  }\n}\n```\n\n### 懒加载\n\n对于大型目录（>1000 个项目）：\n\n```dart\nList<SftpFile> loadPage(String path, int page, int pageSize) {\n  final all = cache[path] ?? [];\n  final start = page * pageSize;\n  final end = start + pageSize;\n  return all.sublist(start, end.clamp(0, all.length));\n}\n```\n\n### 分页\n\n```dart\nclass PaginatedDirectory {\n  static const pageSize = 100;\n\n  Future<List<SftpFile>> getPage(int page) async {\n    final offset = page * pageSize;\n    return await sftp.listDir(\n      path,\n      offset: offset,\n      limit: pageSize,\n    );\n  }\n}\n```\n"
  },
  {
    "path": "docs/src/content/docs/zh/principles/ssh.md",
    "content": "---\ntitle: SSH 连接\ndescription: SSH 连接是如何建立和管理的\n---\n\n了解 Server Box 中的 SSH 连接机制。\n\n## 连接流程\n\n```text\n用户输入 → Spi 配置 → genClient() → SSH 客户端 → 会话 (Session)\n```\n\n### 第一步：配置\n\n`Spi` (Server Parameter Info) 模型包含：\n\n```dart\nclass Spi {\n  String id;         // 唯一标识\n  String name;       // 服务器名称\n  String ip;         // IP 地址\n  int port;          // SSH 端口 (默认 22)\n  String user;       // 用户名\n  String? pwd;       // 密码 (加密存储)\n  String? keyId;     // SSH 密钥 ID\n  String? jumpId;    // 跳板机 ID\n  String? alterUrl;  // 备用 URL\n}\n```\n\n### 第二步：生成客户端\n\n`genClient(spi)` 创建 SSH 客户端：\n\n```dart\nFuture<SSHClient> genClient(Spi spi) async {\n  // 1. 建立 socket\n  var socket = await connect(spi.ip, spi.port);\n\n  // 2. 如果失败，尝试备用 URL\n  if (socket == null && spi.alterUrl != null) {\n    socket = await connect(spi.alterUrl, spi.port);\n  }\n\n  if (socket == null) {\n    throw ConnectionException('Unable to connect');\n  }\n\n  // 3. 身份验证\n  final client = SSHClient(\n    socket: socket,\n    username: spi.user,\n    onPasswordRequest: () => spi.pwd,\n    onIdentityRequest: () => loadKey(spi.keyId),\n  );\n\n  // 4. 验证主机密钥\n  await verifyHostKey(client, spi);\n\n  return client;\n}\n```\n\n### 第三步：跳板机 (如果已配置)\n\n对于跳板机，采用递归连接：\n\n```dart\nif (spi.jumpId != null) {\n  final jumpClient = await genClient(getJumpSpi(spi.jumpId));\n  final forwarded = await jumpClient.forwardLocal(\n    spi.ip,\n    spi.port,\n  );\n  // 通过转发的 socket 进行连接\n}\n```\n\n## 身份验证方式\n\n### 密码验证\n\n```dart\nonPasswordRequest: () => spi.pwd\n```\n\n- 密码以加密形式存储在 Hive 中\n- 连接时解密\n- 发送到服务器进行验证\n\n### 私钥验证\n\n```dart\nonIdentityRequest: () async {\n  final key = await KeyStore.get(spi.keyId);\n  return decyptPem(key.pem, key.password);\n}\n```\n\n**密钥加载流程：**\n1. 从 `KeyStore` 获取加密的密钥\n2. 解密密码（通过生物识别或提示）\n3. 解析 PEM 格式\n4. 标准化换行符 (LF)\n5. 返回用于身份验证\n\n### 键盘交互式 (Keyboard-Interactive)\n\n```dart\nonUserInfoRequest: (instructions) async {\n  // 处理挑战-响应 (Challenge-Response)\n  return responses;\n}\n```\n\n支持：\n- 密码验证\n- OTP 令牌\n- 双因子认证 (2FA)\n\n## 主机密钥验证\n\n### 为什么要验证主机密钥？\n\n通过确保连接的是同一个服务器，防止**中间人 (MITM)** 攻击。\n\n### 存储格式\n\n```text\n{spi.id}::{keyType}\n```\n\n示例：\n```text\nmy-server::ssh-ed25519\nmy-server::ecdsa-sha2-nistp256\n```\n\n### 指纹格式\n\n**MD5 十六进制：**\n```text\naa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99\n```\n\n**Base64：**\n```text\nSHA256:AbCdEf1234567890...=\n```\n\n### 验证流程\n\n```dart\nFuture<void> verifyHostKey(SSHClient client, Spi spi) async {\n  final key = await client.hostKey;\n  final keyType = key.type;\n  final fingerprint = md5Hex(key); // 或 base64\n\n  final stored = SettingStore.sshKnownHostsFingerprints\n      ['${spi.id}::$keyType'];\n\n  if (stored == null) {\n    // 新主机 - 提示用户\n    final trust = await promptUser(\n      '未知主机',\n      '指纹: $fingerprint',\n    );\n    if (trust) {\n      SettingStore.sshKnownHostsFingerprints\n          ['${spi.id}::$keyType'] = fingerprint;\n    }\n  } else if (stored != fingerprint) {\n    // 已更改 - 警告用户\n    await warnUser(\n      '主机密钥已更改！',\n      '可能存在中间人攻击',\n    );\n  }\n}\n```\n\n## 会话管理\n\n### 连接池\n\n在 `ServerProvider` 中维护活动的客户端：\n\n```dart\nclass ServerProvider {\n  final Map<String, SSHClient> _clients = {};\n\n  SSHClient getClient(String spiId) {\n    return _clients[spiId] ??= connect(spiId);\n  }\n}\n```\n\n### 心跳检测 (Keep-Alive)\n\n在闲置期间维持连接：\n\n```dart\nTimer.periodic(\n  Duration(seconds: 30),\n  (_) => client.sendKeepAlive(),\n);\n```\n\n### 自动重连\n\n连接丢失时：\n\n```dart\nclient.onError.listen((error) async {\n  await Future.delayed(Duration(seconds: 5));\n  reconnect();\n});\n```\n\n## 连接生命周期\n\n```text\n┌─────────────┐\n│    初始化   │\n└──────┬──────┘\n       │ connect()\n       ↓\n┌─────────────┐\n│    连接中   │ ←──┐\n└──────┬──────┘   │\n       │ 成功     │\n       ↓          │ 失败 (重试)\n┌─────────────┐   │\n│    已连接   │───┘\n└──────┬──────┘\n       │\n       ↓\n┌─────────────┐\n│    活跃中   │ ──→ 发送命令\n└──────┬──────┘\n       │\n       ↓ (错误/断开)\n┌─────────────┐\n│    已断开   │\n└─────────────┘\n```\n\n## 错误处理\n\n### 连接超时\n\n```dart\ntry {\n  await client.connect().timeout(\n    Duration(seconds: 30),\n  );\n} on TimeoutException {\n  throw ConnectionException('连接超时');\n}\n```\n\n### 身份验证失败\n\n```dart\nonAuthFail: (error) {\n  if (error.contains('password')) {\n    return '密码无效';\n  } else if (error.contains('key')) {\n    return 'SSH 密钥无效';\n  }\n  return '身份验证失败';\n}\n```\n\n### 主机密钥不匹配\n\n```dart\nonHostKeyMismatch: (stored, current) {\n  showSecurityWarning(\n    '主机密钥已更改！',\n    '可能存在中间人攻击',\n  );\n}\n```\n\n## 性能考量\n\n### 连接复用\n\n- 在不同功能间复用客户端\n- 避免不必要的断开和重连\n- 为并发操作建立连接池\n\n### 最佳设置\n\n- **超时时间**：30 秒 (可调)\n- **心跳频率**：每 30 秒一次\n- **重试延迟**：5 秒\n\n### 网络效率\n\n- 单个连接处理多个操作\n- 尽可能使用管道 (Pipeline) 命令\n- 避免打开多个连接\n"
  },
  {
    "path": "docs/src/content/docs/zh/principles/state.md",
    "content": "---\ntitle: 状态管理\ndescription: 如何使用 Riverpod 进行状态管理\n---\n\n了解 Server Box 中的状态管理架构。\n\n## 为何选择 Riverpod？\n\n**主要优势：**\n- **编译时安全**：在编译阶段即可发现错误\n- **无需 BuildContext**：可在任何地方访问状态\n- **易于测试**：方便对 Provider 进行隔离测试\n- **代码生成**：减少样板代码，确保类型安全\n\n## Provider 架构\n\n```\n┌─────────────────────────────────────────────┐\n│         UI 层 (Widgets)                     │\n│  - ConsumerWidget / ConsumerStatefulWidget  │\n│  - ref.watch() / ref.read()                 │\n└─────────────────────────────────────────────┘\n                ↓ 监听 (watches)\n┌─────────────────────────────────────────────┐\n│         Provider 层                         │\n│  - @riverpod 注解                           │\n│  - 生成的 *.g.dart 文件                      │\n└─────────────────────────────────────────────┘\n                ↓ 使用 (uses)\n┌─────────────────────────────────────────────┐\n│         Service / Store 层                  │\n│  - 业务逻辑                                 │\n│  - 数据访问                                 │\n└─────────────────────────────────────────────┘\n```\n\n## 使用的 Provider 类型\n\n### 1. StateProvider (简单状态)\n\n用于简单的可观察状态：\n\n```dart\n@riverpod\nclass ThemeNotifier extends _$ThemeNotifier {\n  @override\n  ThemeMode build() {\n    // 从设置中加载\n    return SettingStore.themeMode;\n  }\n\n  void setTheme(ThemeMode mode) {\n    state = mode;\n    SettingStore.themeMode = mode;  // 持久化存储\n  }\n}\n```\n\n**使用示例：**\n```dart\nclass MyWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final theme = ref.watch(themeNotifierProvider);\n    return Text('当前主题：$theme');\n  }\n}\n```\n\n### 2. AsyncNotifierProvider (异步状态)\n\n用于异步加载的数据：\n\n```dart\n@riverpod\nclass ServerStatus extends _$ServerStatus {\n  @override\n  Future<StatusModel> build(Server server) async {\n    // 初始加载\n    return await fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() async {\n      return await fetchStatus(server);\n    });\n  }\n}\n```\n\n**使用示例：**\n```dart\nfinal status = ref.watch(serverStatusProvider(server));\n\nstatus.when(\n  data: (data) => StatusWidget(data),\n  loading: () => LoadingWidget(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 3. StreamProvider (实时数据)\n\n用于持续的数据流：\n\n```dart\n@riverpod\nStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {\n  final client = ref.watch(sshClientProvider(server));\n  final stream = client.monitorCpu();\n\n  // 当不再被监听时自动释放资源\n  ref.onDispose(() {\n    client.stopMonitoring();\n  });\n\n  return stream;\n}\n```\n\n**使用示例：**\n```dart\nfinal cpu = ref.watch(cpuUsageProvider(server));\n\ncpu.when(\n  data: (usage) => CpuChart(usage),\n  loading: () => CircularProgressIndicator(),\n  error: (error, stack) => ErrorWidget(error),\n)\n```\n\n### 4. Family Provider (带参数)\n\n可以接收参数的 Provider：\n\n```dart\n@riverpod\nFuture<List<Container>> containers(ContainersRef ref, Server server) async {\n  final client = await ref.watch(sshClientProvider(server).future);\n  return await client.listContainers();\n}\n```\n\n**使用示例：**\n```dart\nfinal containers = ref.watch(containersProvider(server));\n\n// 不同的服务器对应不同的缓存状态\nfinal containers2 = ref.watch(containersProvider(server2));\n```\n\n## 状态更新模式\n\n### 直接更新状态\n\n```dart\nref.read(settingsProvider.notifier).updateTheme(darkMode);\n```\n\n### 计算状态 (Computed State)\n\n```dart\n@riverpod\nint totalServers(TotalServersRef ref) {\n  final servers = ref.watch(serversProvider);\n  return servers.length;\n}\n```\n\n### 派生状态 (Derived State)\n\n```dart\n@riverpod\nList<Server> onlineServers(OnlineServersRef ref) {\n  final all = ref.watch(serversProvider);\n  return all.where((s) => s.isOnline).toList();\n}\n```\n\n## 服务器特定状态\n\n### 单服务器 Provider\n\n每个服务器都有独立的状态：\n\n```dart\n@riverpod\nclass ServerProvider extends _$ServerProvider {\n  @override\n  ServerState build(Server server) {\n    return ServerState.disconnected();\n  }\n\n  Future<void> connect() async {\n    state = ServerState.connecting();\n    try {\n      final client = await genClient(server.spi);\n      state = ServerState.connected(client);\n    } catch (e) {\n      state = ServerState.error(e.toString());\n    }\n  }\n}\n```\n\n### Provider 键 (Keys)\n\n```dart\n// 每个服务器都有唯一的 Provider\n@riverpod\nServerStatus serverStatus(ServerStatusRef ref, Server server) {\n  // server.id 被用作 key\n}\n```\n\n## 响应式模式\n\n### 自动刷新\n\n```dart\n@riverpod\nclass AutoRefreshServerStatus extends _$AutoRefreshServerStatus {\n  Timer? _timer;\n\n  @override\n  Future<StatusModel> build(Server server) async {\n    // 启动定时器\n    _timer = Timer.periodic(Duration(seconds: 5), (_) {\n      refresh();\n    });\n\n    ref.onDispose(() {\n      _timer?.cancel();\n    });\n\n    return await fetchStatus(server);\n  }\n\n  Future<void> refresh() async {\n    state = const AsyncValue.loading();\n    state = await AsyncValue.guard(() => fetchStatus(server));\n  }\n}\n```\n\n### 多 Provider 依赖\n\n```dart\n@riverpod\nFuture<SystemInfo> systemInfo(SystemInfoRef ref, Server server) async {\n  // 先等待 SSH 客户端建立连接\n  final client = await ref.watch(sshClientProvider(server).future);\n\n  // 然后获取系统信息\n  return await client.getSystemInfo();\n}\n```\n\n## 状态持久化\n\n### Hive 集成\n\n```dart\n@riverpod\nclass ServerStoreNotifier extends _$ServerStoreNotifier {\n  @override\n  List<Server> build() {\n    // 从 Hive 加载\n    return Hive.box<Server>('servers').values.toList();\n  }\n\n  void addServer(Server server) {\n    state = [...state, server];\n    // 持久化到 Hive\n    Hive.box<Server>('servers').put(server.id, server);\n  }\n\n  void removeServer(String id) {\n    state = state.where((s) => s.id != id).toList();\n    // 从 Hive 中删除\n    Hive.box<Server>('servers').delete(id);\n  }\n}\n```\n\n## 性能优化\n\n- **Provider Keep-Alive**：通过 `@Riverpod(keepAlive: true)` 防止无监听者时自动销毁\n- **选择性监听**：使用 `select` 仅监听状态的特定部分\n- **Provider 缓存**：Family Provider 为每个参数缓存结果\n\n## 最佳实践\n\n1. **就近放置 Provider**：放在消费它的 Widget 附近\n2. **使用代码生成**：始终使用 `@riverpod` 注解\n3. **保持 Provider 专注**：遵循单一职责原则\n4. **处理加载状态**：务必处理 AsyncValue 的各种状态\n5. **及时销毁资源**：在 `ref.onDispose()` 中进行清理\n6. **避免过深的 Provider 树**：保持 Provider 图结构扁平\n"
  },
  {
    "path": "docs/src/content/docs/zh/principles/terminal.md",
    "content": "---\ntitle: 终端实现\ndescription: SSH 终端的内部工作原理\n---\n\nSSH 终端是功能最复杂的模块之一，基于自定义的 xterm.dart 分支构建。\n\n## 架构概览\n\n```\n┌─────────────────────────────────────────────┐\n│              终端 UI 层                     │\n│  - 标签页管理                               │\n│  - 虚拟键盘                                 │\n│  - 文本选择                                 │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│         xterm.dart 模拟器                   │\n│  - PTY (伪终端)                             │\n│  - VT100/ANSI 模拟                          │\n│  - 渲染引擎                                 │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│          SSH 客户端层                       │\n│  - SSH 会话                                 │\n│  - 通道管理                                 │\n│  - 数据流                                   │\n└─────────────────────────────────────────────┘\n                ↓\n┌─────────────────────────────────────────────┐\n│            远程服务器                       │\n│  - Shell 进程                               │\n│  - 命令执行                                 │\n└─────────────────────────────────────────────┘\n```\n\n## 终端会话生命周期\n\n### 1. 创建会话\n\n```dart\nFuture<TerminalSession> createSession(Spi spi) async {\n  // 1. 获取 SSH 客户端\n  final client = await genClient(spi);\n\n  // 2. 创建 PTY\n  final pty = await client.openPty(\n    term: 'xterm-256color',\n    cols: 80,\n    rows: 24,\n  );\n\n  // 3. 初始化终端模拟器\n  final terminal = Terminal(\n    backend: PtyBackend(pty),\n  );\n\n  // 4. 设置调整尺寸处理程序\n  terminal.onResize.listen((size) {\n    pty.resize(size.cols, size.rows);\n  });\n\n  return TerminalSession(\n    terminal: terminal,\n    pty: pty,\n    client: client,\n  );\n}\n```\n\n### 2. 终端模拟\n\nxterm.dart 分支提供：\n\n**VT100/ANSI 模拟：**\n- 光标移动\n- 颜色（支持 256 色）\n- 文本属性（粗体、下划线等）\n- 滚动区域\n- 交替屏幕缓冲区\n\n**渲染：**\n- 基于行的渲染\n- 双向文本支持\n- Unicode/Emoji 支持\n- 优化重绘\n\n### 3. 数据流向\n\n```\n用户输入\n    ↓\n虚拟键盘 / 实体键盘\n    ↓\n终端模拟器 (按键 → 转义序列)\n    ↓\nSSH 通道 (发送)\n    ↓\n远程 PTY\n    ↓\n远程 Shell\n    ↓\n命令输出\n    ↓\nSSH 通道 (接收)\n    ↓\n终端模拟器 (解析 ANSI 编码)\n    ↓\n渲染到屏幕\n```\n\n## 多标签页系统\n\n### 标签页管理\n\n```dart\nclass TerminalTabs {\n  final Map<String, TabData> _tabs = {};\n  String? _activeTabId;\n\n  void createTab(Server server) {\n    final id = _generateTabId(server);\n    _tabs[id] = TabData(\n      id: id,\n      name: _generateTabName(server),\n      session: createSession(server),\n    );\n    _activeTabId = id;\n  }\n\n  String _generateTabName(Server server) {\n    final count = _tabs.values\n        .where((t) => t.name.startsWith(server.name))\n        .length;\n    return count == 0 ? server.name : '${server.name}($count)';\n  }\n}\n```\n\n### 会话持久化\n\n标签页在导航切换时会保持状态：\n- SSH 连接保持活跃\n- 终端状态保留\n- 滚动缓冲区保留\n- 输入历史保留\n\n## 虚拟键盘\n\n### 平台特定实现\n\n**iOS：**\n- 基于 UIView 的自定义键盘\n- 可通过键盘按钮切换\n- 根据焦点自动显示/隐藏\n\n**Android：**\n- 自定义输入法\n- 与系统键盘集成\n- 快速操作按钮\n\n### 键盘按键\n\n| 按钮 | 操作 |\n|--------|--------|\n| **切换 (Toggle)** | 显示/隐藏系统键盘 |\n| **Ctrl** | 发送 Ctrl 修饰符 |\n| **Alt** | 发送 Alt 修饰符 |\n| **SFTP** | 打开当前目录 |\n| **剪贴板 (Clipboard)** | 上下文感知的复制/粘贴 |\n| **脚本 (Snippets)** | 执行命令脚本 |\n\n## 文本选择\n\n1. **长按**：进入选择模式\n2. **拖动**：扩大选择范围\n3. **释放**：复制到剪贴板\n\n## 字体与尺寸\n\n### 尺寸计算\n\n```dart\nclass TerminalDimensions {\n  static Size calculate(double fontSize, Size screenSize) {\n    final charWidth = fontSize * 0.6;  // 等宽字体宽高比\n    final charHeight = fontSize * 1.2;\n\n    final cols = (screenSize.width / charWidth).floor();\n    final rows = (screenSize.height / charHeight).floor();\n\n    return Size(cols.toDouble(), rows.toDouble());\n  }\n}\n```\n\n### 捏合缩放 (Pinch-to-Zoom)\n\n```dart\nGestureDetector(\n  onScaleStart: () => _baseFontSize = currentFontSize,\n  onScaleUpdate: (details) {\n    final newFontSize = _baseFontSize * details.scale;\n    resize(newFontSize);\n  },\n)\n```\n\n## 配色方案\n\n- **浅色 (Light)**：浅色背景，深色文字\n- **深色 (Dark)**：深色背景，浅色文字\n- **AMOLED**：纯黑背景\n\n## 性能优化\n\n- **脏矩形 (Dirty rectangle)**：仅重绘更改的区域\n- **行缓存 (Line caching)**：缓存渲染的行\n- **延迟滚动 (Lazy scrolling)**：长缓冲区的虚拟滚动\n- **批量更新**：合并多次写入\n- **压缩**：压缩滚动缓冲区内容\n- **防抖 (Debouncing)**：对快速输入进行防抖处理\n\n## 特色功能\n\n### 脚本执行\n\n```dart\nvoid executeSnippet(Snippet snippet) {\n  final formatted = formatSnippet(snippet);\n  terminal.paste(formatted);\n  terminal.paste('\\r');  // 执行\n}\n```\n\n### SFTP 快速访问\n\n```dart\nvoid openSftp() async {\n  final cwd = await terminal.getCurrentWorkingDirectory();\n  Navigator.push(\n    context,\n    SftpPage(initialPath: cwd),\n  );\n}\n```\n"
  },
  {
    "path": "docs/src/content/docs/zh/quick-start.mdx",
    "content": "---\ntitle: 快速开始\ndescription: 在几分钟内开始使用 Server Box\n---\n\n按照本快速入门指南连接您的第一台服务器并开始监控。\n\n## 第一步：添加服务器\n\n1. 打开 Server Box\n2. 点击 **+** 按钮添加新服务器\n3. 填写服务器信息：\n   - **名称 (Name)**：服务器的友好名称\n   - **主机 (Host)**：IP 地址或域名\n   - **端口 (Port)**：SSH 端口（默认：22）\n   - **用户 (User)**：SSH 用户名\n   - **密码或密钥 (Password or Key)**：身份验证方式\n\n4. 点击 **保存 (Save)** 添加服务器\n\n## 第二步：连接并监控\n\n1. 点击服务器卡片进行连接\n2. 应用程序将建立 SSH 连接\n3. 您将看到以下内容的实时状态：\n   - CPU 使用率\n   - 内存 (RAM) 和交换分区 (Swap)\n   - 磁盘使用情况\n   - 网络速度\n\n## 第三步：探索功能\n\n连接后，您可以：\n\n- **打开终端**：点击终端按钮以获得完整的 SSH 访问权限\n- **浏览文件**：使用 SFTP 管理文件\n- **管理容器**：查看和控制 Docker/Podman 容器\n- **查看进程**：检查正在运行的进程\n- **运行片段**：执行保存的命令\n\n## 提示\n\n- **生物识别身份验证**：启用 Face ID / Touch ID / 指纹以快速访问（移动端）\n- **主屏幕小组件**：将服务器状态小组件添加到您的主屏幕（iOS/Android）\n- **后台运行**：在后台保持连接（Android）\n"
  },
  {
    "path": "docs/src/content.config.ts",
    "content": "import { defineCollection } from 'astro:content';\nimport { docsLoader } from '@astrojs/starlight/loaders';\nimport { docsSchema } from '@astrojs/starlight/schema';\n\nexport const collections = {\n\tdocs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),\n};\n"
  },
  {
    "path": "docs/src/styles/custom.css",
    "content": "/* Server Box Custom Styles */\n\n:root {\n\t--sl-color-accent: #02569b;\n\t--sl-color-accent-low: #02569b15;\n\t--starlight-cards--border: var(--sl-color-accent-low);\n}\n"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"include\": [\".astro/types.d.ts\", \"**/*\"],\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "A Flutter project which provide charts to display Linux server status and tools to manage server.\nEspecially thanks to dartssh2 & xterm.dart.\n\n* Status chart (CPU, Sensors, GPU...), SSH Term, SFTP, Docker & Pkg & Process...\n* Platform specific: Bio auth、Msg push、Home widget、watchOS App...\n* English, 简体中文; Deutsch, 繁體中文, Indonesian, Français, Dutch; Español, Русский язык, Português, 日本語\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "A server status & toolbox app using Flutter\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/title.txt",
    "content": "ServerBox\n"
  },
  {
    "path": "fastlane/metadata/android/ru/full_description.txt",
    "content": "Проект на базе Flutter, предоставляющий диаграммы состояний серверов под Linux, Unix и Windows и инструменты для управления ими.\n\nОсобая благодарность dartssh2 и xterm.dart.\n\n* Диаграмма состояния (ЦП, датчики, видеокарта…), SSH Term, SFTP, Docker, пакеты, процессы…\n* Платформозависимые: биометрическая аутентификация, push-уведомления, виджет, приложение для watchOS…\n* Многоязычная поддержка: English, 简体中文; Deutsch, 繁體中文, Indonesian, Français, Dutch; Español, Русский язык, Português, 日本語\n"
  },
  {
    "path": "fastlane/metadata/android/ru/short_description.txt",
    "content": "Приложение для мониторинга серверов и набор инструментов управления ими\n"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/full_description.txt",
    "content": "使用 Flutter 开发的 Linux 服务器工具箱，提供服务器状态图表和管理工具。\n特别感谢 dartssh2 & xterm.dart。\n\n特点：\n* 状态图表（CPU、传感器、GPU 等）, SSH 终端, SFTP, Docker & 包 & 进程管理器...\n* 特殊支持：生物认证、推送、桌面小部件、watchOS App、跟随系统颜色...\n* 本地化 (English, 简体中文, Español, Русский язык, Português, 日本語, Deutsch, 繁體中文, Indonesian, Français, Dutch\n"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/short_description.txt",
    "content": "使用 Flutter 开发的服务器状态和工具箱应用\n"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/title.txt",
    "content": "ServerBox\n"
  },
  {
    "path": "fl_build.json",
    "content": "{\n    \"appName\": \"ServerBox\",\n    \"beforeBuild\": \"./make.dart before\"\n}"
  },
  {
    "path": "ios/.gitignore",
    "content": "*.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\n# platform :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\n# Used for flutter lib \"file_picker\"\nPod::PICKER_MEDIA = false\nPod::PICKER_AUDIO = false\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_ios_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "ios/PrivacyInfo.xcprivacy",
    "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>NSPrivacyAccessedAPITypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>NSPrivacyAccessedAPITypeReasons</key>\n\t\t\t<array>\n\t\t\t\t<string>CA92.1</string>\n\t\t\t</array>\n\t\t\t<key>NSPrivacyAccessedAPIType</key>\n\t\t\t<string>NSPrivacyAccessedAPICategoryUserDefaults</string>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport WidgetKit\nimport Flutter\nimport ActivityKit\n\n@main\n@objc class AppDelegate: FlutterAppDelegate {\n    override func application(\n        _ application: UIApplication,\n        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n    ) -> Bool {\n        GeneratedPluginRegistrant.register(with: self)\n    \n        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController\n        // Home widget channel (legacy)\n        let homeWidgetChannel = FlutterMethodChannel(name: \"tech.lolli.toolbox/home_widget\", binaryMessenger: controller.binaryMessenger)\n        homeWidgetChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in\n            if call.method == \"update\" {\n                if #available(iOS 14.0, *) {\n                    WidgetCenter.shared.reloadTimelines(ofKind: \"StatusWidget\")\n                }\n            }\n        })\n\n        // Main channel for cross-platform calls (incl. Live Activities)\n        let mainChannel = FlutterMethodChannel(name: \"tech.lolli.toolbox/main_chan\", binaryMessenger: controller.binaryMessenger)\n        mainChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in\n            switch call.method {\n            case \"updateHomeWidget\":\n                if #available(iOS 14.0, *) {\n                    WidgetCenter.shared.reloadTimelines(ofKind: \"StatusWidget\")\n                }\n                result(nil)\n            case \"startLiveActivity\":\n                if #available(iOS 16.2, *) {\n                    if let payload = call.arguments as? String {\n                        LiveActivityManager.start(json: payload)\n                    }\n                }\n                result(nil)\n            case \"updateLiveActivity\":\n                if #available(iOS 16.2, *) {\n                    if let payload = call.arguments as? String {\n                        LiveActivityManager.update(json: payload)\n                    }\n                }\n                result(nil)\n            case \"stopLiveActivity\":\n                if #available(iOS 16.2, *) {\n                    LiveActivityManager.stop()\n                }\n                result(nil)\n            default:\n                result(FlutterMethodNotImplemented)\n            }\n        })\n        return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n    }\n\n    override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {\n        if url.scheme == \"https\" || url.scheme == \"http\" {\n            UIApplication.shared.open(url)\n        } else {\n            // Pass\n        }\n        return true\n    }\n    \n    override func applicationWillTerminate(_ application: UIApplication) {\n        // Stop Live Activity when app is about to terminate\n        if #available(iOS 16.2, *) {\n            LiveActivityManager.stop()\n        }\n    }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Icon-1024.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"icon-1024 1.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"background.png\",\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"darkbackground.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"LaunchImage.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\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 clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" image=\"LaunchBackground\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"tWc-Dq-wcI\"/>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\"></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=\"leading\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"leading\" id=\"3T2-ad-Qdv\"/>\n                            <constraint firstItem=\"tWc-Dq-wcI\" firstAttribute=\"bottom\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"bottom\" id=\"RPx-PI-7Xg\"/>\n                            <constraint firstItem=\"tWc-Dq-wcI\" firstAttribute=\"top\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"top\" id=\"SdS-ul-q2q\"/>\n                            <constraint firstAttribute=\"trailing\" secondItem=\"tWc-Dq-wcI\" secondAttribute=\"trailing\" id=\"Swv-Gf-Rwn\"/>\n                            <constraint firstAttribute=\"trailing\" secondItem=\"YRO-k0-Ey4\" secondAttribute=\"trailing\" id=\"TQA-XW-tRk\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"bottom\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"bottom\" id=\"duK-uY-Gun\"/>\n                            <constraint firstItem=\"tWc-Dq-wcI\" firstAttribute=\"leading\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"leading\" id=\"kV7-tw-vXt\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"top\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"top\" id=\"xPn-NY-SIU\"/>\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=\"512\" height=\"512\"/>\n        <image name=\"LaunchBackground\" width=\"1\" height=\"1\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"21507\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" colorMatched=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <device id=\"retina6_12\" orientation=\"portrait\" appearance=\"light\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"21505\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\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=\"393\" height=\"852\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"-26\" y=\"-76\"/>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "ios/Runner/Info-Debug.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\t<dict>\n\t\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t\t<true />\n\t\t<key>CFBundleDevelopmentRegion</key>\n\t\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t\t<key>CFBundleExecutable</key>\n\t\t<string>$(EXECUTABLE_NAME)</string>\n\t\t<key>CFBundleIdentifier</key>\n\t\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t\t<key>CFBundleInfoDictionaryVersion</key>\n\t\t<string>6.0</string>\n\t\t<key>CFBundleLocalizations</key>\n\t\t<array>\n\t\t\t<string>en</string>\n\t\t\t<string>zh</string>\n\t\t</array>\n\t\t<key>CFBundleName</key>\n\t\t<string>ServerBox</string>\n\t\t<key>CFBundlePackageType</key>\n\t\t<string>APPL</string>\n\t\t<key>CFBundleShortVersionString</key>\n\t\t<string>$(MARKETING_VERSION)</string>\n\t\t<key>CFBundleSignature</key>\n\t\t<string>????</string>\n\t\t<key>CFBundleVersion</key>\n\t\t<string>$(CURRENT_PROJECT_VERSION)</string>\n\t\t<key>ITSAppUsesNonExemptEncryption</key>\n\t\t<false />\n\t\t<key>LSRequiresIPhoneOS</key>\n\t\t<true />\n\t\t<key>LSSupportsOpeningDocumentsInPlace</key>\n\t\t<true />\n\t\t<key>NSBonjourServices</key>\n\t\t<array>\n\t\t\t<string>_dartobservatory._tcp</string>\n\t\t</array>\n\t\t<key>NSUserActivityTypes</key>\n\t\t<array>\n\t\t\t<string>ConfigurationIntent</string>\n\t\t</array>\n\t\t<key>NSSupportsLiveActivities</key>\n\t\t<true/>\n\t\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t\t<true />\n\t\t<key>UIBackgroundModes</key>\n\t\t<array>\n\t\t\t<string>fetch</string>\n\t\t</array>\n\t\t<key>UILaunchStoryboardName</key>\n\t\t<string>LaunchScreen</string>\n\t\t<key>UIMainStoryboardFile</key>\n\t\t<string>Main</string>\n\t\t<key>UIStatusBarHidden</key>\n\t\t<false />\n\t\t<key>UISupportedInterfaceOrientations</key>\n\t\t<array>\n\t\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t</array>\n\t\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t\t<array>\n\t\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t</array>\n\t\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t\t<false />\n\n\t\t<key>NSLocalNetworkUsageDescription</key>\n\t\t<string>Access your local network to discover and connect to your server.</string>\n\t\t<key>NSFaceIDUsageDescription</key>\n\t\t<string>Required for auth</string>\n\t\t<key>NSCameraUsageDescription</key>\n\t\t<string>Scan QR codes and etc.</string>\n\t\t<key>NSPhotoLibraryUsageDescription</key>\n\t\t<string>Get QR code and etc.</string>\n\t</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/Info-Profile.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\t<dict>\n\t\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t\t<true/>\n\t\t<key>CFBundleDevelopmentRegion</key>\n\t\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t\t<key>CFBundleExecutable</key>\n\t\t<string>$(EXECUTABLE_NAME)</string>\n\t\t<key>CFBundleIdentifier</key>\n\t\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t\t<key>CFBundleInfoDictionaryVersion</key>\n\t\t<string>6.0</string>\n\t\t<key>CFBundleLocalizations</key>\n\t\t<array>\n\t\t\t<string>en</string>\n\t\t\t<string>zh</string>\n\t\t</array>\n\t\t<key>NSSupportsLiveActivities</key>\n\t\t<true/>\n\t\t<key>CFBundleName</key>\n\t\t<string>ServerBox</string>\n\t\t<key>CFBundlePackageType</key>\n\t\t<string>APPL</string>\n\t\t<key>CFBundleShortVersionString</key>\n\t\t<string>$(MARKETING_VERSION)</string>\n\t\t<key>CFBundleSignature</key>\n\t\t<string>????</string>\n\t\t<key>CFBundleVersion</key>\n\t\t<string>$(CURRENT_PROJECT_VERSION)</string>\n\t\t<key>ITSAppUsesNonExemptEncryption</key>\n\t\t<false/>\n\t\t<key>LSRequiresIPhoneOS</key>\n\t\t<true/>\n\t\t<key>LSSupportsOpeningDocumentsInPlace</key>\n\t\t<true/>\n\t\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t\t<true/>\n\t\t<key>UIBackgroundModes</key>\n\t\t<array>\n\t\t\t<string>fetch</string>\n\t\t</array>\n\t\t<key>UILaunchStoryboardName</key>\n\t\t<string>LaunchScreen</string>\n\t\t<key>UIMainStoryboardFile</key>\n\t\t<string>Main</string>\n\t\t<key>UIStatusBarHidden</key>\n\t\t<false/>\n\t\t<key>UISupportedInterfaceOrientations</key>\n\t\t<array>\n\t\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t</array>\n\t\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t\t<array>\n\t\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t</array>\n\t\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t\t<false/>\n\t\t<key>NSBonjourServices</key>\n\t\t<array>\n\t\t\t<string>_dartobservatory._tcp</string>\n\t\t</array>\n\n\t\t<key>NSLocalNetworkUsageDescription</key>\n\t\t<string>Access your local network to discover and connect to your server.</string>\n\t\t<key>NSFaceIDUsageDescription</key>\n\t\t<string>Required for auth</string>\n\t\t<key>NSCameraUsageDescription</key>\n\t\t<string>Scan QR codes and etc.</string>\n\t\t<key>NSPhotoLibraryUsageDescription</key>\n\t\t<string>Get QR code and etc.</string>\n\t</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/Info-Release.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\t<dict>\n\t\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t\t<true />\n\t\t<key>CFBundleDevelopmentRegion</key>\n\t\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t\t<key>CFBundleExecutable</key>\n\t\t<string>$(EXECUTABLE_NAME)</string>\n\t\t<key>CFBundleIdentifier</key>\n\t\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t\t<key>CFBundleInfoDictionaryVersion</key>\n\t\t<string>6.0</string>\n\t\t<key>CFBundleLocalizations</key>\n\t\t<array>\n\t\t\t<string>en</string>\n\t\t\t<string>zh</string>\n\t\t</array>\n\t\t<key>NSSupportsLiveActivities</key>\n\t\t<true/>\n\t\t<key>CFBundleName</key>\n\t\t<string>ServerBox</string>\n\t\t<key>CFBundlePackageType</key>\n\t\t<string>APPL</string>\n\t\t<key>CFBundleShortVersionString</key>\n\t\t<string>$(MARKETING_VERSION)</string>\n\t\t<key>CFBundleSignature</key>\n\t\t<string>????</string>\n\t\t<key>CFBundleVersion</key>\n\t\t<string>$(CURRENT_PROJECT_VERSION)</string>\n\t\t<key>ITSAppUsesNonExemptEncryption</key>\n\t\t<false />\n\t\t<key>LSRequiresIPhoneOS</key>\n\t\t<true />\n\t\t<key>LSSupportsOpeningDocumentsInPlace</key>\n\t\t<true />\n\t\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t\t<true />\n\t\t<key>UIBackgroundModes</key>\n\t\t<array>\n\t\t\t<string>fetch</string>\n\t\t</array>\n\t\t<key>UILaunchStoryboardName</key>\n\t\t<string>LaunchScreen</string>\n\t\t<key>UIMainStoryboardFile</key>\n\t\t<string>Main</string>\n\t\t<key>UIStatusBarHidden</key>\n\t\t<false />\n\t\t<key>UISupportedInterfaceOrientations</key>\n\t\t<array>\n\t\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t</array>\n\t\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t\t<array>\n\t\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t</array>\n\t\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t\t<false />\n\n\t\t<key>NSFaceIDUsageDescription</key>\n\t\t<string>Required for auth</string>\n\t\t<key>NSCameraUsageDescription</key>\n\t\t<string>Scan QR codes and etc.</string>\n\t\t<key>NSPhotoLibraryUsageDescription</key>\n\t\t<string>Get QR code and etc.</string>\n\t</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/LiveActivityManager.swift",
    "content": "//\n//  LiveActivityManager.swift\n//  Runner\n//\n//  Handles starting/updating/stopping Terminal Live Activities from Flutter via MethodChannel.\n//\n\nimport Foundation\nimport ActivityKit\n\n@available(iOS 16.2, *)\nclass LiveActivityManager {\n    static var current: Activity<TerminalAttributes>?\n\n    struct Payload: Decodable {\n        let id: String\n        let title: String\n        let subtitle: String\n        let startTimeMs: Int\n        let status: String\n        let hasTerminal: Bool?\n        let connectionCount: Int?\n    }\n\n    private static func parse(_ json: String) -> Payload? {\n        guard let data = json.data(using: .utf8) else { return nil }\n        return try? JSONDecoder().decode(Payload.self, from: data)\n    }\n\n    static func start(json: String) {\n        guard #available(iOS 16.2, *) else { return }\n        guard let p = parse(json) else { return }\n        let attributes = TerminalAttributes(id: p.id)\n        let date = Date(timeIntervalSince1970: TimeInterval(p.startTimeMs) / 1000.0)\n        // Localize multi-connection title/subtitle on iOS side\n        let isMulti = (p.id == \"multi_connections\")\n        let title = isMulti\n            ? String(format: NSLocalizedString(\"%d connections\", comment: \"Title for multiple connections\"), p.connectionCount ?? 1)\n            : p.title\n        let subtitle = isMulti\n            ? NSLocalizedString(\"Multiple SSH sessions active\", comment: \"Subtitle for multiple connections\")\n            : p.subtitle\n        let state = TerminalAttributes.ContentState(\n            id: p.id,\n            title: title,\n            subtitle: subtitle,\n            status: p.status,\n            startTime: date,\n            hasTerminal: p.hasTerminal ?? true,\n            connectionCount: p.connectionCount ?? 1\n        )\n        let content = ActivityContent(state: state, staleDate: nil)\n        do {\n            current = try Activity<TerminalAttributes>.request(attributes: attributes, content: content, pushType: nil)\n        } catch {\n            // ignore\n        }\n    }\n\n    static func update(json: String) {\n        guard #available(iOS 16.2, *) else { return }\n        guard let p = parse(json) else { return }\n        let date = Date(timeIntervalSince1970: TimeInterval(p.startTimeMs) / 1000.0)\n        // Localize multi-connection title/subtitle on iOS side\n        let isMulti = (p.id == \"multi_connections\")\n        let title = isMulti\n            ? String(format: NSLocalizedString(\"%d connections\", comment: \"Title for multiple connections\"), p.connectionCount ?? 1)\n            : p.title\n        let subtitle = isMulti\n            ? NSLocalizedString(\"Multiple SSH sessions active\", comment: \"Subtitle for multiple connections\")\n            : p.subtitle\n        let state = TerminalAttributes.ContentState(\n            id: p.id,\n            title: title,\n            subtitle: subtitle,\n            status: p.status,\n            startTime: date,\n            hasTerminal: p.hasTerminal ?? true,\n            connectionCount: p.connectionCount ?? 1\n        )\n        if let activity = current {\n            Task { await activity.update(ActivityContent(state: state, staleDate: nil)) }\n        } else {\n            start(json: json)\n        }\n    }\n\n    static func stop() {\n        guard #available(iOS 16.2, *) else { return }\n        if let activity = current {\n            Task { await activity.end(dismissalPolicy: .immediate) }\n            current = nil\n        }\n    }\n}\n"
  },
  {
    "path": "ios/Runner/PrivacyInfo.xcprivacy",
    "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>NSPrivacyAccessedAPITypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>NSPrivacyAccessedAPITypeReasons</key>\n\t\t\t<array>\n\t\t\t\t<string>E174.1</string>\n\t\t\t</array>\n\t\t\t<key>NSPrivacyAccessedAPIType</key>\n\t\t\t<string>NSPrivacyAccessedAPICategoryDiskSpace</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>NSPrivacyAccessedAPITypeReasons</key>\n\t\t\t<array>\n\t\t\t\t<string>CA92.1</string>\n\t\t\t</array>\n\t\t\t<key>NSPrivacyAccessedAPIType</key>\n\t\t\t<string>NSPrivacyAccessedAPICategoryUserDefaults</string>\n\t\t</dict>\n\t</array>\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>production</string>\n\t<key>com.apple.developer.icloud-container-identifiers</key>\n\t<array>\n\t\t<string>iCloud.tech.lolli.serverbox</string>\n\t</array>\n\t<key>com.apple.developer.icloud-services</key>\n\t<array>\n\t\t<string>CloudDocuments</string>\n\t</array>\n\t<key>com.apple.developer.ubiquity-container-identifiers</key>\n\t<array>\n\t\t<string>iCloud.tech.lolli.serverbox</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/TerminalLiveActivityAttributes.swift",
    "content": "//\n//  TerminalLiveActivityAttributes.swift\n//  Runner\n//\n//  Mirror of the ActivityKit attributes used in the extension.\n//\n\nimport Foundation\nimport ActivityKit\n\n@available(iOS 16.1, *)\npublic struct TerminalAttributes: ActivityAttributes {\n    public struct ContentState: Codable, Hashable {\n        public var id: String\n        public var title: String\n        public var subtitle: String\n        public var status: String\n        public var startTime: Date\n        public var hasTerminal: Bool\n        public var connectionCount: Int\n\n        public init(id: String, title: String, subtitle: String, status: String, startTime: Date, hasTerminal: Bool, connectionCount: Int = 1) {\n            self.id = id\n            self.title = title\n            self.subtitle = subtitle\n            self.status = status\n            self.startTime = startTime\n            self.hasTerminal = hasTerminal\n            self.connectionCount = connectionCount\n        }\n    }\n\n    public var id: String\n\n    public init(id: String) {\n        self.id = id\n    }\n}\n\n"
  },
  {
    "path": "ios/Runner/Utils.swift",
    "content": "//\n//  Utils.swift\n//  Runner\n//\n//  Created by lolli on 2023/9/16.\n//\n\nimport Foundation\n\nlet accessoryKey = \"accessory_widget_url\"\n\nlet helpUrl = URL(string: \"https://github.com/lollipopkit/flutter_server_box/wiki#home-widget--watchos-app\")!\n\nextension Date {\n    func toStr() -> String {\n        let formatter = DateFormatter()\n        formatter.dateStyle = .none\n        formatter.timeStyle = .short\n        formatter.locale = Locale.current\n        return formatter.string(from: self)\n    }\n}\n\nenum ErrType: Error {\n    case url(String)\n    case http(String)\n}\n\nenum ContentState {\n    case loading\n    case error(ErrType)\n    case normal(Status)\n}\n\nstruct Status {\n    let name: String\n    let cpu: String\n    let mem: String\n    let disk: String\n    let net: String\n}\n"
  },
  {
    "path": "ios/Runner/de.lproj/LaunchScreen.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/de.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Verbunden\";\n\"Connecting\" = \"Verbindung wird hergestellt\";\n\"Disconnected\" = \"Getrennt\";\n\"Multiple SSH sessions active\" = \"Mehrere aktive SSH-Sitzungen\";\n\"1 connection\" = \"1 Verbindung\";\n\"%d connections\" = \"%d Verbindungen\";\n\n"
  },
  {
    "path": "ios/Runner/de.lproj/Main.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/en.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Connected\";\n\"Connecting\" = \"Connecting\";\n\"Disconnected\" = \"Disconnected\";\n\"Multiple SSH sessions active\" = \"Multiple SSH sessions active\";\n\"1 connection\" = \"1 connection\";\n\"%d connections\" = \"%d connections\";\n\n"
  },
  {
    "path": "ios/Runner/es.lproj/LaunchScreen.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/es.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Conectado\";\n\"Connecting\" = \"Conectando\";\n\"Disconnected\" = \"Desconectado\";\n\"Multiple SSH sessions active\" = \"Varias sesiones SSH activas\";\n\"1 connection\" = \"1 conexión\";\n\"%d connections\" = \"%d conexiones\";\n\n"
  },
  {
    "path": "ios/Runner/es.lproj/Main.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/fr.lproj/LaunchScreen.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/fr.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Connecté\";\n\"Connecting\" = \"Connexion en cours\";\n\"Disconnected\" = \"Déconnecté\";\n\"Multiple SSH sessions active\" = \"Plusieurs sessions SSH actives\";\n\"1 connection\" = \"1 connexion\";\n\"%d connections\" = \"%d connexions\";\n\n"
  },
  {
    "path": "ios/Runner/fr.lproj/Main.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/id.lproj/LaunchScreen.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/id.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Terhubung\";\n\"Connecting\" = \"Menghubungkan\";\n\"Disconnected\" = \"Terputus\";\n\"Multiple SSH sessions active\" = \"Beberapa sesi SSH aktif\";\n\"1 connection\" = \"1 koneksi\";\n\"%d connections\" = \"%d koneksi\";\n\n"
  },
  {
    "path": "ios/Runner/id.lproj/Main.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/ja.lproj/LaunchScreen.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/ja.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"ターミナル\";\n\"Connected\" = \"接続済み\";\n\"Connecting\" = \"接続中\";\n\"Disconnected\" = \"切断\";\n\"Multiple SSH sessions active\" = \"複数の SSH セッションがアクティブ\";\n\"1 connection\" = \"1 件の接続\";\n\"%d connections\" = \"%d 件の接続\";\n\n"
  },
  {
    "path": "ios/Runner/ja.lproj/Main.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/pt-BR.lproj/LaunchScreen.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/pt-BR.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Conectado\";\n\"Connecting\" = \"Conectando\";\n\"Disconnected\" = \"Desconectado\";\n\"Multiple SSH sessions active\" = \"Várias sessões SSH ativas\";\n\"1 connection\" = \"1 conexão\";\n\"%d connections\" = \"%d conexões\";\n\n"
  },
  {
    "path": "ios/Runner/pt-BR.lproj/Main.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/ru.lproj/LaunchScreen.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/ru.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Терминал\";\n\"Connected\" = \"Подключено\";\n\"Connecting\" = \"Подключение\";\n\"Disconnected\" = \"Отключено\";\n\"Multiple SSH sessions active\" = \"Несколько активных сеансов SSH\";\n\"1 connection\" = \"1 подключение\";\n\"%d connections\" = \"%d подключений\";\n\n"
  },
  {
    "path": "ios/Runner/ru.lproj/Main.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/zh-Hans.lproj/LaunchScreen.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/zh-Hans.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"终端\";\n\"Connected\" = \"已连接\";\n\"Connecting\" = \"连接中\";\n\"Disconnected\" = \"已断开连接\";\n\"Multiple SSH sessions active\" = \"多个 SSH 会话正在活动\";\n\"1 connection\" = \"1 个连接\";\n\"%d connections\" = \"%d 个连接\";\n\n"
  },
  {
    "path": "ios/Runner/zh-Hans.lproj/Main.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/zh-Hant.lproj/LaunchScreen.strings",
    "content": "\n"
  },
  {
    "path": "ios/Runner/zh-Hant.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"終端機\";\n\"Connected\" = \"已連線\";\n\"Connecting\" = \"連線中\";\n\"Disconnected\" = \"已中斷連線\";\n\"Multiple SSH sessions active\" = \"多個 SSH 連線運行中\";\n\"1 connection\" = \"1 個連線\";\n\"%d connections\" = \"%d 個連線\";\n\n"
  },
  {
    "path": "ios/Runner/zh-Hant.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\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t4A2DCD6B2E4B127100CF68B7 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD692E4B127100CF68B7 /* LiveActivityManager.swift */; };\n\t\t4A2DCD6C2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD6A2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift */; };\n\t\t4A2DCD6F2E4B128100CF68B7 /* TerminalLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD6D2E4B128100CF68B7 /* TerminalLiveActivity.swift */; };\n\t\t4A2DCD702E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD6E2E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t7538AEC32BB83FAB002AB82A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7538AEC22BB83FAB002AB82A /* PrivacyInfo.xcprivacy */; };\n\t\t7538AEC52BB83FC8002AB82A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7538AEC42BB83FC8002AB82A /* PrivacyInfo.xcprivacy */; };\n\t\t7538AEC72BB83FD3002AB82A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7538AEC62BB83FD3002AB82A /* PrivacyInfo.xcprivacy */; };\n\t\t9234EA956D1225D388A01660 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 278C1EB3935F9285537B0516 /* Pods_Runner.framework */; };\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\tE33A3E372A626DCD009744AB /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E33A3E362A626DCD009744AB /* WidgetKit.framework */; };\n\t\tE33A3E392A626DCD009744AB /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E33A3E382A626DCD009744AB /* SwiftUI.framework */; };\n\t\tE33A3E3C2A626DCE009744AB /* StatusWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A3E3B2A626DCE009744AB /* StatusWidgetBundle.swift */; };\n\t\tE33A3E402A626DCE009744AB /* StatusWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A3E3F2A626DCE009744AB /* StatusWidget.swift */; };\n\t\tE33A3E492A626DD0009744AB /* StatusWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E33A3E352A626DCD009744AB /* StatusWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };\n\t\tE37C48EA2B9C30EE00E542D2 /* StatusWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = E37C48ED2B9C30EE00E542D2 /* StatusWidget.intentdefinition */; };\n\t\tE37C48EB2B9C30EE00E542D2 /* StatusWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = E37C48ED2B9C30EE00E542D2 /* StatusWidget.intentdefinition */; };\n\t\tE39515CA2AB5AD62003602C1 /* WatchEndApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E39515C92AB5AD62003602C1 /* WatchEndApp.swift */; };\n\t\tE39515CC2AB5AD62003602C1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E39515CB2AB5AD62003602C1 /* ContentView.swift */; };\n\t\tE39515D42AB5AD64003602C1 /* ServerBox.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = E39515C72AB5AD62003602C1 /* ServerBox.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };\n\t\tE39515DB2AB5AE7F003602C1 /* PhoneConnMgr.swift in Sources */ = {isa = PBXBuildFile; fileRef = E39515DA2AB5AE7F003602C1 /* PhoneConnMgr.swift */; };\n\t\tE39515DD2AB5AE9E003602C1 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = E39515DC2AB5AE9E003602C1 /* Store.swift */; };\n\t\tE39515E02AB5CF97003602C1 /* Watch.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E39515DF2AB5CF97003602C1 /* Watch.xcassets */; };\n\t\tE39A76AE2AB9A2F70067C641 /* Info-Debug.plist in Resources */ = {isa = PBXBuildFile; fileRef = E39A76AB2AB9A2F70067C641 /* Info-Debug.plist */; };\n\t\tE39A76AF2AB9A2F70067C641 /* Info-Release.plist in Resources */ = {isa = PBXBuildFile; fileRef = E39A76AC2AB9A2F70067C641 /* Info-Release.plist */; };\n\t\tE39A76B02AB9A2F70067C641 /* Info-Profile.plist in Resources */ = {isa = PBXBuildFile; fileRef = E39A76AD2AB9A2F70067C641 /* Info-Profile.plist */; };\n\t\tE3AE8AEA2AB601DB000A6459 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3AE8AE92AB601DB000A6459 /* Utils.swift */; };\n\t\tE3AE8AEB2AB601DB000A6459 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3AE8AE92AB601DB000A6459 /* Utils.swift */; };\n\t\tE3AE8AEC2AB601DB000A6459 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3AE8AE92AB601DB000A6459 /* Utils.swift */; };\n\t\tE3DB67ED2A31FE200027B8CB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E3DB67EB2A31FE200027B8CB /* LaunchScreen.storyboard */; };\n        F0A1B2C31A2B3C4D5E6F0005 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F0A1B2C31A2B3C4D5E6F0001 /* Localizable.strings */; };\n        F0A1B2C31A2B3C4D5E6F1005 /* Localizable.strings (StatusWidget) in Resources */ = {isa = PBXBuildFile; fileRef = F0A1B2C31A2B3C4D5E6F1001 /* Localizable.strings */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\tE33A3E472A626DD0009744AB /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 97C146E61CF9000F007C117D /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = E33A3E342A626DCD009744AB;\n\t\t\tremoteInfo = StatusWidgetExtension;\n\t\t};\n\t\tE39515D22AB5AD64003602C1 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 97C146E61CF9000F007C117D /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = E39515C62AB5AD62003602C1;\n\t\t\tremoteInfo = \"WatchEnd Watch App\";\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\tE33A3E4A2A626DD0009744AB /* 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\tE33A3E492A626DD0009744AB /* StatusWidgetExtension.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\t\tE39515D52AB5AD64003602C1 /* Embed Watch Content */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"$(CONTENTS_FOLDER_PATH)/Watch\";\n\t\t\tdstSubfolderSpec = 16;\n\t\t\tfiles = (\n\t\t\t\tE39515D42AB5AD64003602C1 /* ServerBox.app in Embed Watch Content */,\n\t\t\t);\n\t\t\tname = \"Embed Watch Content\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\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\t278C1EB3935F9285537B0516 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; 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\t4A2DCD692E4B127100CF68B7 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = \"<group>\"; };\n\t\t4A2DCD6A2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalLiveActivityAttributes.swift; sourceTree = \"<group>\"; };\n\t\t4A2DCD6D2E4B128100CF68B7 /* TerminalLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalLiveActivity.swift; sourceTree = \"<group>\"; };\n\t\t4A2DCD6E2E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalLiveActivityAttributes.swift; sourceTree = \"<group>\"; };\n\t\t5A4B3EB10512B2EB8E10213B /* 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\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\t7538AEC22BB83FAB002AB82A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = \"<group>\"; };\n\t\t7538AEC42BB83FC8002AB82A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = \"<group>\"; };\n\t\t7538AEC62BB83FD3002AB82A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.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\t9C5314B89F1F73A1900CCAFD /* 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\tA775F241DEE026555178AC01 /* 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\tE33A3E352A626DCD009744AB /* StatusWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = \"wrapper.app-extension\"; includeInIndex = 0; path = StatusWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tE33A3E362A626DCD009744AB /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };\n\t\tE33A3E382A626DCD009744AB /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };\n\t\tE33A3E3B2A626DCE009744AB /* StatusWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusWidgetBundle.swift; sourceTree = \"<group>\"; };\n\t\tE33A3E3F2A626DCE009744AB /* StatusWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusWidget.swift; sourceTree = \"<group>\"; };\n\t\tE33A3E442A626DD0009744AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\tE37C48EC2B9C30EE00E542D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/StatusWidget.intentdefinition; sourceTree = \"<group>\"; };\n\t\tE37C48F02B9C5E0C00E542D2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/StatusWidget.strings; sourceTree = \"<group>\"; };\n\t\tE37C48F22B9C5E0D00E542D2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/StatusWidget.strings\"; sourceTree = \"<group>\"; };\n\t\tE37C48F42B9C5E1000E542D2 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hant\"; path = \"zh-Hant.lproj/StatusWidget.strings\"; sourceTree = \"<group>\"; };\n\t\tE37C48F62B9C5E1500E542D2 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/StatusWidget.strings; sourceTree = \"<group>\"; };\n\t\tE37C48F82B9C5E1700E542D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/StatusWidget.strings; sourceTree = \"<group>\"; };\n\t\tE37C48FA2B9C5E1700E542D2 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/StatusWidget.strings; sourceTree = \"<group>\"; };\n\t\tE37C48FC2B9C5E1800E542D2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/StatusWidget.strings; sourceTree = \"<group>\"; };\n\t\tE37C48FE2B9C5E1800E542D2 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"pt-BR\"; path = \"pt-BR.lproj/StatusWidget.strings\"; sourceTree = \"<group>\"; };\n\t\tE37C49002B9C5E1900E542D2 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/StatusWidget.strings; sourceTree = \"<group>\"; };\n\t\tE37C49022B9C5E1900E542D2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/StatusWidget.strings; sourceTree = \"<group>\"; };\n\t\tE39515C72AB5AD62003602C1 /* ServerBox.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ServerBox.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tE39515C92AB5AD62003602C1 /* WatchEndApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchEndApp.swift; sourceTree = \"<group>\"; };\n\t\tE39515CB2AB5AD62003602C1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = \"<group>\"; };\n\t\tE39515DA2AB5AE7F003602C1 /* PhoneConnMgr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneConnMgr.swift; sourceTree = \"<group>\"; };\n\t\tE39515DC2AB5AE9E003602C1 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = \"<group>\"; };\n\t\tE39515DF2AB5CF97003602C1 /* Watch.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Watch.xcassets; sourceTree = \"<group>\"; };\n\t\tE398BF6A29BDB34500FE4FD5 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = \"<group>\"; };\n\t\tE39A76AB2AB9A2F70067C641 /* Info-Debug.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = \"Info-Debug.plist\"; sourceTree = \"<group>\"; };\n\t\tE39A76AC2AB9A2F70067C641 /* Info-Release.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = \"Info-Release.plist\"; sourceTree = \"<group>\"; };\n\t\tE39A76AD2AB9A2F70067C641 /* Info-Profile.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = \"Info-Profile.plist\"; sourceTree = \"<group>\"; };\n\t\tE3AE8AE92AB601DB000A6459 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = \"<group>\"; };\n\t\tE3D26BC22B99637800D83425 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hant\"; path = \"zh-Hant.lproj/Main.strings\"; sourceTree = \"<group>\"; };\n\t\tE3D26BC32B99637900D83425 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hant\"; path = \"zh-Hant.lproj/LaunchScreen.strings\"; sourceTree = \"<group>\"; };\n\t\tE3D26BC42B99637F00D83425 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BC52B99637F00D83425 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/LaunchScreen.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BC62B99639800D83425 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Main.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BC72B99639800D83425 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BC82B9963A600D83425 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Main.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BC92B9963A600D83425 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/LaunchScreen.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BCA2B9963AC00D83425 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Main.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BCB2B9963AC00D83425 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/LaunchScreen.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BCC2B9963BC00D83425 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"pt-BR\"; path = \"pt-BR.lproj/Main.strings\"; sourceTree = \"<group>\"; };\n\t\tE3D26BCD2B9963BC00D83425 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"pt-BR\"; path = \"pt-BR.lproj/LaunchScreen.strings\"; sourceTree = \"<group>\"; };\n\t\tE3D26BCE2B9963CE00D83425 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Main.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BCF2B9963CE00D83425 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/LaunchScreen.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BD02B9963E100D83425 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/Main.strings\"; sourceTree = \"<group>\"; };\n\t\tE3D26BD12B9963E100D83425 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/LaunchScreen.strings\"; sourceTree = \"<group>\"; };\n\t\tE3D26BD22B9966EC00D83425 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Main.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BD32B9966EC00D83425 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/LaunchScreen.strings; sourceTree = \"<group>\"; };\n\t\tE3DB67EC2A31FE200027B8CB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F0002 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F0003 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/Localizable.strings\"; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F0004 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hant\"; path = \"zh-Hant.lproj/Localizable.strings\"; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F0006 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F0007 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F0008 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F0009 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F000A /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"pt-BR\"; path = \"pt-BR.lproj/Localizable.strings\"; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F000B /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F000C /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F1002 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F1003 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/Localizable.strings\"; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F1004 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hant\"; path = \"zh-Hant.lproj/Localizable.strings\"; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F1006 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F1007 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F1008 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F1009 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F100A /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"pt-BR\"; path = \"pt-BR.lproj/Localizable.strings\"; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F100B /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n        F0A1B2C31A2B3C4D5E6F100C /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t9234EA956D1225D388A01660 /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tE33A3E322A626DCD009744AB /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tE33A3E392A626DCD009744AB /* SwiftUI.framework in Frameworks */,\n\t\t\t\tE33A3E372A626DCD009744AB /* WidgetKit.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tE39515C42AB5AD62003602C1 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t65C40392B90925608A0465EE /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tA775F241DEE026555178AC01 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t9C5314B89F1F73A1900CCAFD /* Pods-Runner.release.xcconfig */,\n\t\t\t\t5A4B3EB10512B2EB8E10213B /* Pods-Runner.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\tE33A3E3A2A626DCE009744AB /* StatusWidget */,\n\t\t\t\tE39515C82AB5AD62003602C1 /* WatchApp */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t65C40392B90925608A0465EE /* Pods */,\n\t\t\t\tD242A20E381A343934B6A7B6 /* 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\tE33A3E352A626DCD009744AB /* StatusWidgetExtension.appex */,\n\t\t\t\tE39515C72AB5AD62003602C1 /* ServerBox.app */,\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                F0A1B2C31A2B3C4D5E6F0001 /* Localizable.strings */,\n\t\t\t\t7538AEC22BB83FAB002AB82A /* PrivacyInfo.xcprivacy */,\n\t\t\t\tE398BF6A29BDB34500FE4FD5 /* Runner.entitlements */,\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\tE3DB67EB2A31FE200027B8CB /* LaunchScreen.storyboard */,\n\t\t\t\tE39A76AB2AB9A2F70067C641 /* Info-Debug.plist */,\n\t\t\t\tE39A76AD2AB9A2F70067C641 /* Info-Profile.plist */,\n\t\t\t\tE39A76AC2AB9A2F70067C641 /* Info-Release.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t4A2DCD692E4B127100CF68B7 /* LiveActivityManager.swift */,\n\t\t\t\t4A2DCD6A2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\tE3AE8AE92AB601DB000A6459 /* Utils.swift */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD242A20E381A343934B6A7B6 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t278C1EB3935F9285537B0516 /* Pods_Runner.framework */,\n\t\t\t\tE33A3E362A626DCD009744AB /* WidgetKit.framework */,\n\t\t\t\tE33A3E382A626DCD009744AB /* SwiftUI.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tE33A3E3A2A626DCE009744AB /* StatusWidget */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n                F0A1B2C31A2B3C4D5E6F1001 /* Localizable.strings */,\n\t\t\t\t7538AEC42BB83FC8002AB82A /* PrivacyInfo.xcprivacy */,\n\t\t\t\tE33A3E3B2A626DCE009744AB /* StatusWidgetBundle.swift */,\n\t\t\t\t4A2DCD6D2E4B128100CF68B7 /* TerminalLiveActivity.swift */,\n\t\t\t\t4A2DCD6E2E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift */,\n\t\t\t\tE33A3E3F2A626DCE009744AB /* StatusWidget.swift */,\n\t\t\t\tE37C48ED2B9C30EE00E542D2 /* StatusWidget.intentdefinition */,\n\t\t\t\tE33A3E442A626DD0009744AB /* Info.plist */,\n\t\t\t);\n\t\t\tpath = StatusWidget;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tE39515C82AB5AD62003602C1 /* WatchApp */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t7538AEC62BB83FD3002AB82A /* PrivacyInfo.xcprivacy */,\n\t\t\t\tE39515C92AB5AD62003602C1 /* WatchEndApp.swift */,\n\t\t\t\tE39515CB2AB5AD62003602C1 /* ContentView.swift */,\n\t\t\t\tE39515DA2AB5AE7F003602C1 /* PhoneConnMgr.swift */,\n\t\t\t\tE39515DC2AB5AE9E003602C1 /* Store.swift */,\n\t\t\t\tE39515DF2AB5CF97003602C1 /* Watch.xcassets */,\n\t\t\t);\n\t\t\tpath = WatchApp;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\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\tD539DA9CBCC9FFF884ED67C9 /* [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\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\tAA0A1FF2F3246F84EB0D91F2 /* [CP] Embed Pods Frameworks */,\n\t\t\t\tE33A3E4A2A626DD0009744AB /* Embed Foundation Extensions */,\n\t\t\t\tE39515D52AB5AD64003602C1 /* Embed Watch Content */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\tE33A3E482A626DD0009744AB /* PBXTargetDependency */,\n\t\t\t\tE39515D32AB5AD64003602C1 /* 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\tE33A3E342A626DCD009744AB /* StatusWidgetExtension */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = E33A3E4E2A626DD0009744AB /* Build configuration list for PBXNativeTarget \"StatusWidgetExtension\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tE33A3E312A626DCD009744AB /* Sources */,\n\t\t\t\tE33A3E322A626DCD009744AB /* Frameworks */,\n\t\t\t\tE33A3E332A626DCD009744AB /* 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 = StatusWidgetExtension;\n\t\t\tproductName = StatusWidgetExtension;\n\t\t\tproductReference = E33A3E352A626DCD009744AB /* StatusWidgetExtension.appex */;\n\t\t\tproductType = \"com.apple.product-type.app-extension\";\n\t\t};\n\t\tE39515C62AB5AD62003602C1 /* WatchApp */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = E39515D92AB5AD64003602C1 /* Build configuration list for PBXNativeTarget \"WatchApp\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tE39515C32AB5AD62003602C1 /* Sources */,\n\t\t\t\tE39515C42AB5AD62003602C1 /* Frameworks */,\n\t\t\t\tE39515C52AB5AD62003602C1 /* 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 = WatchApp;\n\t\t\tproductName = \"WatchEnd Watch App\";\n\t\t\tproductReference = E39515C72AB5AD62003602C1 /* ServerBox.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\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 = 1430;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\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\tE33A3E342A626DCD009744AB = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.3;\n\t\t\t\t\t};\n\t\t\t\t\tE39515C62AB5AD62003602C1 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.3.1;\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 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\t\"zh-Hant\",\n\t\t\t\tfr,\n\t\t\t\tru,\n\t\t\t\tes,\n\t\t\t\tde,\n\t\t\t\t\"pt-BR\",\n\t\t\t\tid,\n\t\t\t\t\"zh-Hans\",\n\t\t\t\tja,\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\tE33A3E342A626DCD009744AB /* StatusWidgetExtension */,\n\t\t\t\tE39515C62AB5AD62003602C1 /* WatchApp */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\tE3DB67ED2A31FE200027B8CB /* LaunchScreen.storyboard in Resources */,\n\t\t\t\tE39A76AF2AB9A2F70067C641 /* Info-Release.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\tE39A76AE2AB9A2F70067C641 /* Info-Debug.plist in Resources */,\n\t\t\t\tE39A76B02AB9A2F70067C641 /* Info-Profile.plist in Resources */,\n\t\t\t\t7538AEC32BB83FAB002AB82A /* PrivacyInfo.xcprivacy in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n                F0A1B2C31A2B3C4D5E6F0005 /* Localizable.strings in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tE33A3E332A626DCD009744AB /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t7538AEC52BB83FC8002AB82A /* PrivacyInfo.xcprivacy in Resources */,\n                F0A1B2C31A2B3C4D5E6F1005 /* Localizable.strings (StatusWidget) in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tE39515C52AB5AD62003602C1 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t7538AEC72BB83FD3002AB82A /* PrivacyInfo.xcprivacy in Resources */,\n\t\t\t\tE39515E02AB5CF97003602C1 /* Watch.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\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\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\tAA0A1FF2F3246F84EB0D91F2 /* [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\tD539DA9CBCC9FFF884ED67C9 /* [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/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t\tE37C48EA2B9C30EE00E542D2 /* StatusWidget.intentdefinition in Sources */,\n\t\t\t\tE3AE8AEA2AB601DB000A6459 /* Utils.swift in Sources */,\n\t\t\t\t4A2DCD6B2E4B127100CF68B7 /* LiveActivityManager.swift in Sources */,\n\t\t\t\t4A2DCD6C2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tE33A3E312A626DCD009744AB /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tE33A3E402A626DCE009744AB /* StatusWidget.swift in Sources */,\n\t\t\t\tE37C48EB2B9C30EE00E542D2 /* StatusWidget.intentdefinition in Sources */,\n\t\t\t\t4A2DCD6F2E4B128100CF68B7 /* TerminalLiveActivity.swift in Sources */,\n\t\t\t\t4A2DCD702E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift in Sources */,\n\t\t\t\tE33A3E3C2A626DCE009744AB /* StatusWidgetBundle.swift in Sources */,\n\t\t\t\tE3AE8AEB2AB601DB000A6459 /* Utils.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tE39515C32AB5AD62003602C1 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tE3AE8AEC2AB601DB000A6459 /* Utils.swift in Sources */,\n\t\t\t\tE39515DB2AB5AE7F003602C1 /* PhoneConnMgr.swift in Sources */,\n\t\t\t\tE39515CC2AB5AD62003602C1 /* ContentView.swift in Sources */,\n\t\t\t\tE39515CA2AB5AD62003602C1 /* WatchEndApp.swift in Sources */,\n\t\t\t\tE39515DD2AB5AE9E003602C1 /* Store.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\tE33A3E482A626DD0009744AB /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = E33A3E342A626DCD009744AB /* StatusWidgetExtension */;\n\t\t\ttargetProxy = E33A3E472A626DD0009744AB /* PBXContainerItemProxy */;\n\t\t};\n\t\tE39515D32AB5AD64003602C1 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = E39515C62AB5AD62003602C1 /* WatchApp */;\n\t\t\ttargetProxy = E39515D22AB5AD64003602C1 /* 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\tE3D26BC22B99637800D83425 /* zh-Hant */,\n\t\t\t\tE3D26BC42B99637F00D83425 /* fr */,\n\t\t\t\tE3D26BC62B99639800D83425 /* ru */,\n\t\t\t\tE3D26BC82B9963A600D83425 /* es */,\n\t\t\t\tE3D26BCA2B9963AC00D83425 /* de */,\n\t\t\t\tE3D26BCC2B9963BC00D83425 /* pt-BR */,\n\t\t\t\tE3D26BCE2B9963CE00D83425 /* id */,\n\t\t\t\tE3D26BD02B9963E100D83425 /* zh-Hans */,\n\t\t\t\tE3D26BD22B9966EC00D83425 /* ja */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tE37C48ED2B9C30EE00E542D2 /* StatusWidget.intentdefinition */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\tE37C48EC2B9C30EE00E542D2 /* Base */,\n\t\t\t\tE37C48F02B9C5E0C00E542D2 /* en */,\n\t\t\t\tE37C48F22B9C5E0D00E542D2 /* zh-Hans */,\n\t\t\t\tE37C48F42B9C5E1000E542D2 /* zh-Hant */,\n\t\t\t\tE37C48F62B9C5E1500E542D2 /* fr */,\n\t\t\t\tE37C48F82B9C5E1700E542D2 /* de */,\n\t\t\t\tE37C48FA2B9C5E1700E542D2 /* id */,\n\t\t\t\tE37C48FC2B9C5E1800E542D2 /* ja */,\n\t\t\t\tE37C48FE2B9C5E1800E542D2 /* pt-BR */,\n\t\t\t\tE37C49002B9C5E1900E542D2 /* ru */,\n\t\t\t\tE37C49022B9C5E1900E542D2 /* es */,\n\t\t\t);\n\t\t\tname = StatusWidget.intentdefinition;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tE3DB67EB2A31FE200027B8CB /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\tE3DB67EC2A31FE200027B8CB /* Base */,\n\t\t\t\tE3D26BC32B99637900D83425 /* zh-Hant */,\n\t\t\t\tE3D26BC52B99637F00D83425 /* fr */,\n\t\t\t\tE3D26BC72B99639800D83425 /* ru */,\n\t\t\t\tE3D26BC92B9963A600D83425 /* es */,\n\t\t\t\tE3D26BCB2B9963AC00D83425 /* de */,\n\t\t\t\tE3D26BCD2B9963BC00D83425 /* pt-BR */,\n\t\t\t\tE3D26BCF2B9963CE00D83425 /* id */,\n\t\t\t\tE3D26BD12B9963E100D83425 /* zh-Hans */,\n\t\t\t\tE3D26BD32B9966EC00D83425 /* ja */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n        F0A1B2C31A2B3C4D5E6F0001 /* Localizable.strings */ = {\n            isa = PBXVariantGroup;\n            children = (\n                F0A1B2C31A2B3C4D5E6F0002 /* en */,\n                F0A1B2C31A2B3C4D5E6F0003 /* zh-Hans */,\n                F0A1B2C31A2B3C4D5E6F0004 /* zh-Hant */,\n                F0A1B2C31A2B3C4D5E6F0006 /* fr */,\n                F0A1B2C31A2B3C4D5E6F0007 /* ru */,\n                F0A1B2C31A2B3C4D5E6F0008 /* es */,\n                F0A1B2C31A2B3C4D5E6F0009 /* de */,\n                F0A1B2C31A2B3C4D5E6F000A /* pt-BR */,\n                F0A1B2C31A2B3C4D5E6F000B /* id */,\n                F0A1B2C31A2B3C4D5E6F000C /* ja */,\n            );\n            name = Localizable.strings;\n            sourceTree = \"<group>\";\n        };\n        F0A1B2C31A2B3C4D5E6F1001 /* Localizable.strings */ = {\n            isa = PBXVariantGroup;\n            children = (\n                F0A1B2C31A2B3C4D5E6F1002 /* en */,\n                F0A1B2C31A2B3C4D5E6F1003 /* zh-Hans */,\n                F0A1B2C31A2B3C4D5E6F1004 /* zh-Hant */,\n                F0A1B2C31A2B3C4D5E6F1006 /* fr */,\n                F0A1B2C31A2B3C4D5E6F1007 /* ru */,\n                F0A1B2C31A2B3C4D5E6F1008 /* es */,\n                F0A1B2C31A2B3C4D5E6F1009 /* de */,\n                F0A1B2C31A2B3C4D5E6F100A /* pt-BR */,\n                F0A1B2C31A2B3C4D5E6F100B /* id */,\n                F0A1B2C31A2B3C4D5E6F100C /* ja */,\n            );\n            name = Localizable.strings;\n            sourceTree = \"<group>\";\n        };\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 = 1331;\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = \"Runner/Info-$(CONFIGURATION).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\tMARKETING_VERSION = 1.0.1331;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;\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\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 = 1331;\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = \"Runner/Info-$(CONFIGURATION).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\tMARKETING_VERSION = 1.0.1331;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;\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 = 1331;\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = \"Runner/Info-$(CONFIGURATION).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\tMARKETING_VERSION = 1.0.1331;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;\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\tE33A3E4B2A626DD0009744AB /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;\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_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_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1331;\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = StatusWidget/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = StatusWidget;\n\t\t\t\tINFOPLIST_KEY_NSHumanReadableCopyright = \"\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.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.1331;\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.lollipopkit.toolbox.StatusWidget;\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_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\tE33A3E4C2A626DD0009744AB /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;\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_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_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1331;\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = StatusWidget/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = StatusWidget;\n\t\t\t\tINFOPLIST_KEY_NSHumanReadableCopyright = \"\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.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.1331;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;\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_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\tE33A3E4D2A626DD0009744AB /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;\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_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_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1331;\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = StatusWidget/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = StatusWidget;\n\t\t\t\tINFOPLIST_KEY_NSHumanReadableCopyright = \"\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.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.1331;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;\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_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\t\tE39515D62AB5AD64003602C1 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\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_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_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1331;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\";\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown\";\n\t\t\t\tINFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.lollipopkit.toolbox;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0.1331;\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.lollipopkit.toolbox.WatchEnd;\n\t\t\t\tPRODUCT_NAME = ServerBox;\n\t\t\t\tSDKROOT = watchos;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSUPPORTED_PLATFORMS = \"watchsimulator watchos\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 4;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 7.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tE39515D72AB5AD64003602C1 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\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_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_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1331;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\";\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown\";\n\t\t\t\tINFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.lollipopkit.toolbox;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0.1331;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;\n\t\t\t\tPRODUCT_NAME = ServerBox;\n\t\t\t\tSDKROOT = watchos;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSUPPORTED_PLATFORMS = \"watchsimulator watchos\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 4;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 7.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tE39515D82AB5AD64003602C1 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\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_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_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1331;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\";\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown\";\n\t\t\t\tINFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.lollipopkit.toolbox;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0.1331;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;\n\t\t\t\tPRODUCT_NAME = ServerBox;\n\t\t\t\tSDKROOT = watchos;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSUPPORTED_PLATFORMS = \"watchsimulator watchos\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 4;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 7.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\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\tE33A3E4E2A626DD0009744AB /* Build configuration list for PBXNativeTarget \"StatusWidgetExtension\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tE33A3E4B2A626DD0009744AB /* Debug */,\n\t\t\t\tE33A3E4C2A626DD0009744AB /* Release */,\n\t\t\t\tE33A3E4D2A626DD0009744AB /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tE39515D92AB5AD64003602C1 /* Build configuration list for PBXNativeTarget \"WatchApp\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tE39515D62AB5AD64003602C1 /* Debug */,\n\t\t\t\tE39515D72AB5AD64003602C1 /* Release */,\n\t\t\t\tE39515D82AB5AD64003602C1 /* 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      </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.xcodeproj/xcshareddata/xcschemes/StatusWidgetExtension.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1430\"\n   wasCreatedForAppExtension = \"YES\"\n   version = \"2.0\">\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 = \"E33A3E342A626DCD009744AB\"\n               BuildableName = \"StatusWidgetExtension.appex\"\n               BlueprintName = \"StatusWidgetExtension\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\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      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      shouldAutocreateTestPlan = \"YES\">\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"\"\n      selectedLauncherIdentifier = \"Xcode.IDEFoundation.Launcher.PosixSpawn\"\n      launchStyle = \"0\"\n      askForAppToLaunch = \"Yes\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\"\n      launchAutomaticallySubstyle = \"2\">\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      <EnvironmentVariables>\n         <EnvironmentVariable\n            key = \"_XCWidgetKind\"\n            value = \"\"\n            isEnabled = \"NO\">\n         </EnvironmentVariable>\n         <EnvironmentVariable\n            key = \"_XCWidgetDefaultView\"\n            value = \"timeline\"\n            isEnabled = \"NO\">\n         </EnvironmentVariable>\n         <EnvironmentVariable\n            key = \"_XCWidgetFamily\"\n            value = \"medium\"\n            isEnabled = \"NO\">\n         </EnvironmentVariable>\n      </EnvironmentVariables>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      askForAppToLaunch = \"Yes\"\n      launchAutomaticallySubstyle = \"2\">\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.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1430\"\n   version = \"1.7\">\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 = \"E39515C62AB5AD62003602C1\"\n               BuildableName = \"ServerBox.app\"\n               BlueprintName = \"WatchApp\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\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      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      shouldAutocreateTestPlan = \"YES\">\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"E39515C62AB5AD62003602C1\"\n            BuildableName = \"ServerBox.app\"\n            BlueprintName = \"WatchApp\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"E39515C62AB5AD62003602C1\"\n            BuildableName = \"ServerBox.app\"\n            BlueprintName = \"WatchApp\"\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/StatusWidget/Base.lproj/StatusWidget.intentdefinition",
    "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>INEnums</key>\n\t<array/>\n\t<key>INIntentDefinitionModelVersion</key>\n\t<string>1.2</string>\n\t<key>INIntentDefinitionNamespace</key>\n\t<string>88xZPY</string>\n\t<key>INIntentDefinitionSystemVersion</key>\n\t<string>22F82</string>\n\t<key>INIntentDefinitionToolsBuildVersion</key>\n\t<string>14E222b</string>\n\t<key>INIntentDefinitionToolsVersion</key>\n\t<string>14.3</string>\n\t<key>INIntents</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>INIntentCategory</key>\n\t\t\t<string>information</string>\n\t\t\t<key>INIntentDescription</key>\n\t\t\t<string>Config for home status widget</string>\n\t\t\t<key>INIntentDescriptionID</key>\n\t\t\t<string>tVvJ9c</string>\n\t\t\t<key>INIntentEligibleForWidgets</key>\n\t\t\t<true/>\n\t\t\t<key>INIntentIneligibleForSuggestions</key>\n\t\t\t<true/>\n\t\t\t<key>INIntentLastParameterTag</key>\n\t\t\t<integer>2</integer>\n\t\t\t<key>INIntentName</key>\n\t\t\t<string>Configuration</string>\n\t\t\t<key>INIntentParameters</key>\n\t\t\t<array>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>INIntentParameterConfigurable</key>\n\t\t\t\t\t<true/>\n\t\t\t\t\t<key>INIntentParameterDisplayName</key>\n\t\t\t\t\t<string>Url</string>\n\t\t\t\t\t<key>INIntentParameterDisplayNameID</key>\n\t\t\t\t\t<string>5QiOUM</string>\n\t\t\t\t\t<key>INIntentParameterDisplayPriority</key>\n\t\t\t\t\t<integer>1</integer>\n\t\t\t\t\t<key>INIntentParameterMetadata</key>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>INIntentParameterMetadataCapitalization</key>\n\t\t\t\t\t\t<string>Characters</string>\n\t\t\t\t\t\t<key>INIntentParameterMetadataDefaultValue</key>\n\t\t\t\t\t\t<string>https://</string>\n\t\t\t\t\t\t<key>INIntentParameterMetadataDefaultValueID</key>\n\t\t\t\t\t\t<string>wwvqC2</string>\n\t\t\t\t\t\t<key>INIntentParameterMetadataDisableAutocorrect</key>\n\t\t\t\t\t\t<true/>\n\t\t\t\t\t\t<key>INIntentParameterMetadataDisableSmartDashes</key>\n\t\t\t\t\t\t<true/>\n\t\t\t\t\t\t<key>INIntentParameterMetadataDisableSmartQuotes</key>\n\t\t\t\t\t\t<true/>\n\t\t\t\t\t</dict>\n\t\t\t\t\t<key>INIntentParameterName</key>\n\t\t\t\t\t<string>url</string>\n\t\t\t\t\t<key>INIntentParameterPromptDialogs</key>\n\t\t\t\t\t<array>\n\t\t\t\t\t\t<dict>\n\t\t\t\t\t\t\t<key>INIntentParameterPromptDialogCustom</key>\n\t\t\t\t\t\t\t<true/>\n\t\t\t\t\t\t\t<key>INIntentParameterPromptDialogType</key>\n\t\t\t\t\t\t\t<string>Configuration</string>\n\t\t\t\t\t\t</dict>\n\t\t\t\t\t\t<dict>\n\t\t\t\t\t\t\t<key>INIntentParameterPromptDialogCustom</key>\n\t\t\t\t\t\t\t<true/>\n\t\t\t\t\t\t\t<key>INIntentParameterPromptDialogFormatString</key>\n\t\t\t\t\t\t\t<string>Server monitor url</string>\n\t\t\t\t\t\t\t<key>INIntentParameterPromptDialogFormatStringID</key>\n\t\t\t\t\t\t\t<string>j41owf</string>\n\t\t\t\t\t\t\t<key>INIntentParameterPromptDialogType</key>\n\t\t\t\t\t\t\t<string>Primary</string>\n\t\t\t\t\t\t</dict>\n\t\t\t\t\t</array>\n\t\t\t\t\t<key>INIntentParameterSupportsResolution</key>\n\t\t\t\t\t<true/>\n\t\t\t\t\t<key>INIntentParameterTag</key>\n\t\t\t\t\t<integer>1</integer>\n\t\t\t\t\t<key>INIntentParameterType</key>\n\t\t\t\t\t<string>String</string>\n\t\t\t\t</dict>\n\t\t\t</array>\n\t\t\t<key>INIntentResponse</key>\n\t\t\t<dict>\n\t\t\t\t<key>INIntentResponseCodes</key>\n\t\t\t\t<array>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>INIntentResponseCodeName</key>\n\t\t\t\t\t\t<string>success</string>\n\t\t\t\t\t\t<key>INIntentResponseCodeSuccess</key>\n\t\t\t\t\t\t<true/>\n\t\t\t\t\t</dict>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>INIntentResponseCodeName</key>\n\t\t\t\t\t\t<string>failure</string>\n\t\t\t\t\t</dict>\n\t\t\t\t</array>\n\t\t\t</dict>\n\t\t\t<key>INIntentTitle</key>\n\t\t\t<string>StatusWidgetConf</string>\n\t\t\t<key>INIntentTitleID</key>\n\t\t\t<string>gpCwrM</string>\n\t\t\t<key>INIntentType</key>\n\t\t\t<string>Custom</string>\n\t\t\t<key>INIntentVerb</key>\n\t\t\t<string>View</string>\n\t\t</dict>\n\t</array>\n\t<key>INTypes</key>\n\t<array/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/StatusWidget/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>NSExtensionAttributes</key>\n\t\t<dict>\n\t\t\t<key>IntentsSupportedIntents</key>\n\t\t\t<array>\n\t\t\t\t<string>ConfigurationIntent</string>\n\t\t\t</array>\n\t\t\t<key>NSSupportsLiveActivities</key>\n\t\t\t<true/>\n\t\t</dict>\n\t\t<key>NSExtensionPointIdentifier</key>\n\t\t<string>com.apple.widgetkit-extension</string>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/StatusWidget/PrivacyInfo.xcprivacy",
    "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>NSPrivacyAccessedAPITypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>NSPrivacyAccessedAPITypeReasons</key>\n\t\t\t<array>\n\t\t\t\t<string>CA92.1</string>\n\t\t\t</array>\n\t\t\t<key>NSPrivacyAccessedAPIType</key>\n\t\t\t<string>NSPrivacyAccessedAPICategoryUserDefaults</string>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/StatusWidget/StatusWidget.swift",
    "content": "//\n//  StatusWidget.swift\n//  StatusWidget\n//\n//  Created by lolli on 2023/7/15.\n//\n\nimport WidgetKit\nimport SwiftUI\nimport Intents\nimport AppIntents\nimport Foundation\n\nlet demoStatus = Status(name: \"Server\", cpu: \"31.7%\", mem: \"1.3g / 1.9g\", disk: \"7.1g / 30.0g\", net: \"712.3k / 1.2m\")\nlet domain = \"com.lollipopkit.toolbox\"\nlet bgColor = DynamicColor(dark: UIColor.black, light: UIColor.white)\n\n// Widget-specific constants\nenum WidgetConstants {\n    enum Dimensions {\n        static let smallGauge: CGFloat = 56\n        static let mediumGauge: CGFloat = 64\n        static let largeGauge: CGFloat = 76\n        static let refreshIconSmall: CGFloat = 12\n        static let refreshIconLarge: CGFloat = 14\n        static let cornerRadius: CGFloat = 12\n        static let shadowRadius: CGFloat = 2\n    }\n    enum Thresholds {\n        static let warningThreshold: Double = 0.6\n        static let criticalThreshold: Double = 0.85\n    }\n    enum Spacing {\n        static let tight: CGFloat = 4\n        static let normal: CGFloat = 8\n        static let loose: CGFloat = 12\n        static let extraLoose: CGFloat = 16\n    }\n    enum Colors {\n        static let cardBackground = Color(.systemBackground)\n        static let secondaryText = Color(.secondaryLabel)\n        static let success = Color(.systemGreen)\n        static let warning = Color(.systemOrange)\n        static let critical = Color(.systemRed)\n        static let accent = Color(.systemBlue)\n    }\n    static let appGroupId = \"group.com.lollipopkit.toolbox\"\n}\n\n// Performance optimization: cache parsed values\nstruct ParseCache {\n    private static var percentCache: [String: Double] = [:]\n    private static var usagePercentCache: [String: Double] = [:]\n    \n    static func parsePercent(_ text: String) -> Double {\n        if let cached = percentCache[text] { return cached }\n        let trimmed = text.trimmingCharacters(in: CharacterSet(charactersIn: \"% \"))\n        let result = Double(trimmed).map { max(0, min(1, $0 / 100.0)) } ?? 0\n        percentCache[text] = result\n        return result\n    }\n    \n    static func parseUsagePercent(_ text: String) -> Double {\n        if let cached = usagePercentCache[text] { return cached }\n        let parts = text.split(separator: \"/\").map { String($0).trimmingCharacters(in: .whitespaces) }\n        guard parts.count == 2 else { return 0 }\n        let used = PerformanceUtils.parseSizeToBytes(parts[0])\n        let total = PerformanceUtils.parseSizeToBytes(parts[1])\n        let result = total <= 0 ? 0 : max(0, min(1, used / total))\n        usagePercentCache[text] = result\n        return result\n    }\n    \n    static func parseNetworkTotal(_ text: String) -> (totalBytes: Double, displayText: String) {\n        let parts = text.split(separator: \"/\").map { String($0).trimmingCharacters(in: .whitespaces) }\n        guard parts.count == 2 else { return (0, \"0 B\") }\n        let upload = PerformanceUtils.parseSizeToBytes(parts[0])\n        let download = PerformanceUtils.parseSizeToBytes(parts[1])\n        let total = upload + download\n        let displayText = PerformanceUtils.formatSize(total)\n        return (total, displayText)\n    }\n    \n    static func parseNetworkPercent(_ text: String) -> Double {\n        let parts = text.split(separator: \"/\").map { String($0).trimmingCharacters(in: .whitespaces) }\n        guard parts.count == 2 else { return 0 }\n        let upload = PerformanceUtils.parseSizeToBytes(parts[0])\n        let download = PerformanceUtils.parseSizeToBytes(parts[1])\n        let total = upload + download\n        // Return upload percentage of total traffic\n        return total <= 0 ? 0 : max(0, min(1, upload / total))\n    }\n}\n\nstruct PerformanceUtils {\n    // Precomputed multipliers for performance\n    private static let sizeMultipliers: [Character: Double] = [\n        \"k\": 1024,\n        \"m\": pow(1024, 2),\n        \"g\": pow(1024, 3),\n        \"t\": pow(1024, 4),\n        \"p\": pow(1024, 5)\n    ]\n    \n    static func parseSizeToBytes(_ text: String) -> Double {\n        let lower = text.lowercased().replacingOccurrences(of: \"b\", with: \"\")\n        let unitChar = lower.trimmingCharacters(in: .whitespaces).last\n        let numberPart: String\n        let multiplier: Double\n        \n        if let u = unitChar, let mult = sizeMultipliers[u] {\n            multiplier = mult\n            numberPart = String(lower.dropLast())\n        } else {\n            multiplier = 1.0\n            numberPart = lower\n        }\n        \n        let value = Double(numberPart.trimmingCharacters(in: .whitespaces)) ?? 0\n        return value * multiplier\n    }\n    \n    static func percentStr(_ value: Double) -> String {\n        let pct = max(0, min(1, value)) * 100\n        let rounded = (pct * 10).rounded() / 10\n        return rounded.truncatingRemainder(dividingBy: 1) == 0 \n            ? String(format: \"%.0f%%\", rounded)\n            : String(format: \"%.1f%%\", rounded)\n    }\n    \n    static func thresholdColor(_ value: Double) -> Color {\n        let v = max(0, min(1, value))\n        switch v {\n        case ..<WidgetConstants.Thresholds.warningThreshold: return WidgetConstants.Colors.success\n        case ..<WidgetConstants.Thresholds.criticalThreshold: return WidgetConstants.Colors.warning\n        default: return WidgetConstants.Colors.critical\n        }\n    }\n    \n    static func formatSize(_ bytes: Double) -> String {\n        let units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"]\n        var size = bytes\n        var unitIndex = 0\n        \n        while size >= 1024 && unitIndex < units.count - 1 {\n            size /= 1024\n            unitIndex += 1\n        }\n        \n        return String(format: \"%.1f %@\", size, units[unitIndex])\n    }\n}\n\nstruct Provider: IntentTimelineProvider {\n    func placeholder(in context: Context) -> SimpleEntry {\n        SimpleEntry(date: Date(), configuration: ConfigurationIntent(), state: .normal(demoStatus))\n    }\n\n    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {\n        let entry = SimpleEntry(date: Date(), configuration: configuration, state: .normal(demoStatus))\n        completion(entry)\n    }\n\n    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {\n        var url = configuration.url\n        \n        let family = context.family\n        #if os(iOS)\n        if #available(iOSApplicationExtension 16.0, *) {\n            if family == .accessoryInline || family == .accessoryRectangular {\n                url = UserDefaults(suiteName: WidgetConstants.appGroupId)?.string(forKey: \"accessory_widget_url\")\n            }\n        }\n        #endif\n\n        let currentDate = Date()\n        let refreshDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!\n        fetch(url: url) { result in\n            let entry: SimpleEntry = SimpleEntry(\n                date: currentDate,\n                configuration: configuration,\n                state: result\n            )\n            let timeline = Timeline(entries: [entry], policy: .after(refreshDate))\n            completion(timeline)\n        }\n    }\n    \n    func fetch(url: String?, completion: @escaping (ContentState) -> Void) {\n        guard let url = url, url.count >= 12 else {\n            completion(.error(.url(\"url is nil OR len < 12\")))\n            return\n        }\n        guard let url = URL(string: url) else {\n            completion(.error(.url(\"parse url failed\")))\n            return\n        }\n\n        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in\n            if error != nil {\n                completion(.error(.http(error?.localizedDescription ?? \"unknown http err\")))\n                return\n            }\n            guard let data = data else {\n                completion(.error(.http(\"empty http data\")))\n                return\n            }\n            let jsonAll = try! JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]\n            let code = jsonAll[\"code\"] as? Int ?? 1\n            if (code != 0) {\n                let msg = jsonAll[\"msg\"] as? String ?? \"Empty err\"\n                completion(.error(.http(msg)))\n                return\n            }\n\n            let json = jsonAll[\"data\"] as? [String: Any] ?? [:]\n            let name = json[\"name\"] as? String ?? \"\"\n            let disk = json[\"disk\"] as? String ?? \"\"\n            let cpu = json[\"cpu\"] as? String ?? \"\"\n            let mem = json[\"mem\"] as? String ?? \"\"\n            let net = json[\"net\"] as? String ?? \"\"\n            completion(.normal(Status(name: name, cpu: cpu, mem: mem, disk: disk, net: net)))\n        }\n        task.resume()\n    }\n}\n\nstruct SimpleEntry: TimelineEntry {\n    let date: Date\n    let configuration: ConfigurationIntent\n    let state: ContentState\n}\n\nstruct StatusWidgetEntryView : View {\n    var entry: Provider.Entry\n\n    @Environment(\\.widgetFamily) var family: WidgetFamily\n\n    var body: some View {\n        switch entry.state {\n            case .loading:\n                Text(\"Loading\").widgetBackground()\n            case .error(let err):\n                switch err {\n                case .http(let description):\n                    VStack(alignment: .center) {\n                        Text(description)\n                        if #available(iOS 17.0, *) {\n                            Button(intent: RefreshIntent()) {\n                                Image(systemName: \"arrow.clockwise\")\n                                    .resizable()\n                                    .frame(width: WidgetConstants.Dimensions.refreshIconSmall, height: WidgetConstants.Dimensions.refreshIconSmall * 1.27)\n                            }.tint(.gray)\n                        }\n                    }\n                    .widgetBackground()\n                case .url(_):\n                    Link(\"Open wiki ⬅️\", destination: helpUrl)\n                    .widgetBackground()\n                }\n            case .normal(let data):\n                let sumColor: Color = .primary.opacity(0.7)\n                switch family {\n                    case .systemMedium:\n                        VStack(alignment: .leading, spacing: WidgetConstants.Spacing.normal) {\n                            // Title + refresh\n                            if #available(iOS 17.0, *) {\n                                HStack {\n                                    Text(data.name).font(.system(.title3, design: .monospaced))\n                                    Spacer()\n                                    Button(intent: RefreshIntent()) {\n                                        Image(systemName: \"arrow.clockwise\")\n                                            .resizable()\n                                            .frame(width: WidgetConstants.Dimensions.refreshIconSmall, height: WidgetConstants.Dimensions.refreshIconSmall * 1.27)\n                                    }.tint(.gray)\n                                }\n                            } else {\n                                Text(data.name).font(.system(.title3, design: .monospaced))\n                            }\n                            Spacer(minLength: WidgetConstants.Spacing.normal)\n                            // Gauges row\n                            HStack(spacing: WidgetConstants.Spacing.tight) {\n                                GaugeTile(label: \"CPU\", value: ParseCache.parsePercent(data.cpu), display: data.cpu, diameter: WidgetConstants.Dimensions.smallGauge)\n                                GaugeTile(label: \"MEM\", value: ParseCache.parseUsagePercent(data.mem), display: PerformanceUtils.percentStr(ParseCache.parseUsagePercent(data.mem)), diameter: WidgetConstants.Dimensions.smallGauge)\n                                GaugeTile(label: \"DISK\", value: ParseCache.parseUsagePercent(data.disk), display: PerformanceUtils.percentStr(ParseCache.parseUsagePercent(data.disk)), diameter: WidgetConstants.Dimensions.smallGauge)\n                                GaugeTile(label: \"NET\", value: ParseCache.parseNetworkPercent(data.net), display: ParseCache.parseNetworkTotal(data.net).displayText, diameter: WidgetConstants.Dimensions.smallGauge)\n                            }\n                            .frame(maxWidth: .infinity, alignment: .leading)\n                                .padding(.bottom, 3)\n                        }\n                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)\n                        .autoPadding()\n                        .widgetBackground()\n                    #if os(iOS)\n                    case .accessoryRectangular:\n                        VStack(alignment: .leading, spacing: 2) {\n                            HStack {\n                                Text(data.name)\n                                    .font(.system(size: 15, weight: .semibold, design: .monospaced))\n                                Spacer()\n                                CirclePercent(percent: data.cpu)\n                                    .padding(.top, 3)\n                                    .padding(.trailing, 3)\n                            }\n                            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)\n                            Spacer()\n                            DetailItem(icon: \"memorychip\", text: data.mem, color: sumColor)\n                            DetailItem(icon: \"externaldrive\", text: data.disk, color: sumColor)\n                            DetailItem(icon: \"network\", text: data.net, color: sumColor)\n                        }\n                        .widgetBackground()\n                    case .accessoryInline:\n                        Text(\"\\(data.name) \\(data.cpu)\").widgetBackground()\n                    #endif\n                    default:\n                        VStack(alignment: .leading, spacing: 3.7) {\n                            if #available(iOS 17.0, *) {\n                                HStack {\n                                    Text(data.name).font(.system(.title3, design: .monospaced))\n                                    Spacer()\n                                    Button(intent: RefreshIntent()) {\n                                        Image(systemName: \"arrow.clockwise\")\n                                            .resizable()\n                                            .frame(width: WidgetConstants.Dimensions.refreshIconSmall, height: WidgetConstants.Dimensions.refreshIconSmall * 1.27)\n                                    }.tint(.gray)\n                                }\n                            } else {\n                                Text(data.name).font(.system(.title3, design: .monospaced))\n                            }\n                            Spacer()\n                            DetailItem(icon: \"cpu\", text: data.cpu, color: sumColor)\n                            DetailItem(icon: \"memorychip\", text: data.mem, color: sumColor)\n                            DetailItem(icon: \"externaldrive\", text: data.disk, color: sumColor)\n                            DetailItem(icon: \"network\", text: data.net, color: sumColor)\n                        }\n                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)\n                        .autoPadding()\n                        .widgetBackground()\n                }\n        }\n    }\n}\n\nextension View {\n    @ViewBuilder\n    func widgetBackground() -> some View {\n        // Modern card-style background with subtle effects\n        let backgroundView = LinearGradient(\n            gradient: Gradient(colors: [\n                Color(bgColor.resolve()),\n                Color(bgColor.resolve()).opacity(0.95)\n            ]),\n            startPoint: .topLeading,\n            endPoint: .bottomTrailing\n        )\n        \n        if #available(iOS 17.0, *) {\n            containerBackground(for: .widget) {\n                backgroundView\n            }\n        } else {\n            background(backgroundView)\n        }\n    }\n    \n    // Enhanced padding with improved spacing\n    func autoPadding() -> some View {\n        if #available(iOS 17.0, *) {\n            return self.padding(.all, WidgetConstants.Spacing.tight)\n        } else {\n            return self.padding(.all, WidgetConstants.Spacing.extraLoose + 1)\n        }\n    }\n    \n    // Modern card container with shadow and rounded corners\n    func modernCard(cornerRadius: CGFloat = WidgetConstants.Dimensions.cornerRadius) -> some View {\n        self\n            .background(\n                RoundedRectangle(cornerRadius: cornerRadius)\n                    .fill(WidgetConstants.Colors.cardBackground)\n                    .shadow(\n                        color: .black.opacity(0.08),\n                        radius: WidgetConstants.Dimensions.shadowRadius,\n                        x: 0,\n                        y: 1\n                    )\n            )\n    }\n}\n\nstruct StatusWidget: Widget {\n    let kind: String = \"StatusWidget\"\n\n    var body: some WidgetConfiguration {\n        let cfg = IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in\n            StatusWidgetEntryView(entry: entry)\n        }\n        .configurationDisplayName(\"Status\")\n        .description(\"Status of your servers.\")\n        #if os(iOS)\n        if #available(iOSApplicationExtension 16.0, *) {\n            return cfg.supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular, .accessoryInline])\n        } else {\n            return cfg.supportedFamilies([.systemSmall, .systemMedium])\n        }\n        #else\n            return cfg.supportedFamilies([.systemSmall, .systemMedium])\n        #endif\n    }\n}\n\nstruct StatusWidget_Previews: PreviewProvider {\n    static var previews: some View {\n        StatusWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), state: .normal(demoStatus)))\n            .previewContext(WidgetPreviewContext(family: .systemSmall))\n    }\n}\n\nstruct DetailItem: View {\n    let icon: String\n    let text: String\n    let color: Color\n    \n    var body: some View {\n        HStack(spacing: WidgetConstants.Spacing.normal) {\n            Image(systemName: icon)\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .foregroundColor(color.opacity(0.8))\n                .frame(width: 12, height: 12)\n                .background(\n                    Circle()\n                        .fill(color.opacity(0.1))\n                        .frame(width: 20, height: 20)\n                )\n            \n            Text(text)\n                .font(.system(size: 12, weight: .medium, design: .rounded))\n                .foregroundColor(color)\n                .lineLimit(1)\n                .minimumScaleFactor(0.8)\n        }\n        .padding(.horizontal, WidgetConstants.Spacing.tight)\n        .padding(.vertical, 2)\n    }\n}\n\n// Enhanced circular progress indicator\nstruct CirclePercent: View {\n    // eg: 31.7%\n    let percent: String\n    @State private var animatedProgress: Double = 0\n    \n    var body: some View {\n        let percentD = Double(percent.trimmingCharacters(in: .init(charactersIn: \"%\")))\n        let progress = (percentD ?? 0) / 100\n        \n        ZStack {\n            // Background circle\n            Circle()\n                .stroke(Color.primary.opacity(0.15), lineWidth: 2.5)\n            \n            // Progress circle with gradient\n            Circle()\n                .trim(from: 0, to: CGFloat(max(0, min(1, animatedProgress))))\n                .stroke(\n                    AngularGradient(\n                        gradient: Gradient(colors: [\n                            PerformanceUtils.thresholdColor(progress).opacity(0.7),\n                            PerformanceUtils.thresholdColor(progress)\n                        ]),\n                        center: .center\n                    ),\n                    style: StrokeStyle(lineWidth: 3, lineCap: .round)\n                )\n                .rotationEffect(.degrees(-90))\n            \n            // Percentage text\n            Text(percent)\n                .font(.system(size: 8, weight: .bold, design: .rounded))\n                .foregroundColor(.primary.opacity(0.8))\n        }\n        .frame(width: 24, height: 24)\n        .onAppear {\n            withAnimation(.easeOut(duration: 0.8).delay(0.2)) {\n                animatedProgress = progress\n            }\n        }\n        .onChange(of: progress) { newProgress in\n            withAnimation(.easeInOut(duration: 0.5)) {\n                animatedProgress = newProgress\n            }\n        }\n    }\n}\n\n// Modern gauge tile with enhanced visual design\nstruct GaugeTile: View {\n    let label: String\n    // 0..1\n    let value: Double\n    // eg: \"31.7%\"\n    let display: String\n    let diameter: CGFloat\n    \n    @State private var animatedValue: Double = 0\n    \n    var body: some View {\n        VStack(spacing: WidgetConstants.Spacing.normal) {\n            ZStack {\n                // Background circle with subtle shadow effect\n                Circle()\n                    .stroke(Color.primary.opacity(0.1), lineWidth: 4)\n                    .background(\n                        Circle()\n                            .fill(WidgetConstants.Colors.cardBackground)\n                            .shadow(color: .black.opacity(0.05), radius: WidgetConstants.Dimensions.shadowRadius, x: 0, y: 1)\n                    )\n                \n                // Progress arc with gradient effect\n                Circle()\n                    .trim(from: 0, to: CGFloat(max(0, min(1, animatedValue))))\n                    .stroke(\n                        AngularGradient(\n                            gradient: Gradient(colors: [\n                                PerformanceUtils.thresholdColor(value).opacity(0.8),\n                                PerformanceUtils.thresholdColor(value)\n                            ]),\n                            center: .center,\n                            startAngle: .degrees(-90),\n                            endAngle: .degrees(270)\n                        ),\n                        style: StrokeStyle(lineWidth: 5, lineCap: .round)\n                    )\n                    .rotationEffect(.degrees(-90))\n                \n                // Center value text with improved typography\n                Text(display)\n                    .font(.system(size: diameter < 60 ? 11 : 13, weight: .bold, design: .rounded))\n                    .foregroundColor(.primary)\n                    .minimumScaleFactor(0.8)\n                    .lineLimit(1)\n            }\n            .frame(width: diameter, height: diameter)\n            .onAppear {\n                withAnimation(.easeOut(duration: 0.8).delay(0.1)) {\n                    animatedValue = value\n                }\n            }\n            .onChange(of: value) { newValue in\n                withAnimation(.easeInOut(duration: 0.6)) {\n                    animatedValue = newValue\n                }\n            }\n            \n            // Label with enhanced styling\n            if #available(iOS 16.0, *) {\n                Text(label)\n                    .font(.system(size: 11, weight: .medium, design: .rounded))\n                    .foregroundColor(WidgetConstants.Colors.secondaryText)\n                    .textCase(.uppercase)\n                    .tracking(0.5)\n            } else {\n                Text(label)\n                    .font(.system(size: 11, weight: .medium, design: .rounded))\n                    .foregroundColor(WidgetConstants.Colors.secondaryText)\n                    .textCase(.uppercase)\n            }\n        }\n        .frame(maxWidth: .infinity)\n    }\n}\n\n// Legacy functions maintained for compatibility - now delegate to optimized versions\nfunc parsePercent(_ text: String) -> Double {\n    return ParseCache.parsePercent(text)\n}\n\nfunc parseUsagePercent(_ text: String) -> Double {\n    return ParseCache.parseUsagePercent(text)\n}\n\nfunc parseSizeToBytes(_ text: String) -> Double {\n    return PerformanceUtils.parseSizeToBytes(text)\n}\n\nfunc percentStr(_ value: Double) -> String {\n    return PerformanceUtils.percentStr(value)\n}\n\nfunc thresholdColor(_ value: Double) -> Color {\n    return PerformanceUtils.thresholdColor(value)\n}\n\nstruct DynamicColor {\n    let dark: UIColor\n    let light: UIColor\n    \n    func resolve() -> UIColor {\n        if #available(iOS 13, *) {  // 版本号大于等于13\n            return UIColor { (traitCollection: UITraitCollection) -> UIColor in\n                return traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ?\n                    self.dark : self.light\n            }\n        }\n        return self.light\n    }\n}\n\n@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *)\nstruct RefreshIntent: AppIntent {\n    static var title: LocalizedStringResource = \"Refresh\"\n    static var description = IntentDescription(\"Refresh status.\")\n    \n    func perform() async throws -> some IntentResult {\n        return .result()\n    }\n}\n"
  },
  {
    "path": "ios/StatusWidget/StatusWidgetBundle.swift",
    "content": "//\n//  StatusWidgetBundle.swift\n//  StatusWidget\n//\n//  Created by lolli on 2023/7/15.\n//\n\nimport WidgetKit\nimport SwiftUI\n\n@main\nstruct StatusWidgetBundle: WidgetBundle {\n    var body: some Widget {\n        StatusWidget()\n        if #available(iOSApplicationExtension 16.1, *) {\n            TerminalLiveActivity()\n        }\n    }\n}\n"
  },
  {
    "path": "ios/StatusWidget/TerminalLiveActivity.swift",
    "content": "//\n//  TerminalLiveActivity.swift\n//  StatusWidget\n//\n//  Renders the Live Activity UI for SSH/Terminal sessions.\n//\n\nimport SwiftUI\nimport WidgetKit\nimport ActivityKit\n\n// Helper to map status strings to a color dot (case-insensitive).\n@inline(__always)\nprivate func getStatusDotColor(_ status: String) -> Color {\n    switch status.lowercased() {\n    case \"connected\":\n        return .green\n    case \"connecting\":\n        return .yellow\n    case \"disconnected\":\n        return .red\n    default:\n        return .secondary\n    }\n}\n\n// Normalize status for display: capitalize first letter only.\n@inline(__always)\nprivate func formatStatus(_ status: String) -> String {\n    let trimmed = status.trimmingCharacters(in: .whitespacesAndNewlines)\n    guard let first = trimmed.first else { return status }\n    let head = String(first).uppercased()\n    let tail = String(trimmed.dropFirst()).lowercased()\n    return head + tail\n}\n\n// Localize known statuses; fall back to formatted original.\n@inline(__always)\nprivate func localizedStatus(_ status: String) -> String {\n    switch status.lowercased() {\n    case \"connected\":\n        return NSLocalizedString(\"Connected\", comment: \"Session connected status\")\n    case \"connecting\":\n        return NSLocalizedString(\"Connecting\", comment: \"Session connecting status\")\n    case \"disconnected\":\n        return NSLocalizedString(\"Disconnected\", comment: \"Session disconnected status\")\n    default:\n        return formatStatus(status)\n    }\n}\n\n@available(iOS 16.1, *)\nstruct TerminalLiveActivity: Widget {\n    var body: some WidgetConfiguration {\n        ActivityConfiguration(for: TerminalAttributes.self) { context in\n            let state = context.state\n\n            HStack(alignment: .center, spacing: 12) {\n                VStack(alignment: .leading, spacing: 6) {\n                    HStack(spacing: 6) {\n                        Text(state.hasTerminal ? NSLocalizedString(\"Terminal\", comment: \"Terminal label\") : \"SSH\")\n                            .font(.caption)\n                            .foregroundStyle(.secondary)\n                        if state.connectionCount > 1 {\n                            Text(\"(\\(state.connectionCount))\")\n                                .font(.caption)\n                                .foregroundStyle(.secondary)\n                        }\n                    }\n                    Text(state.title)\n                        .font(.headline)\n                        .lineLimit(1)\n                        .truncationMode(.tail)\n                    Text(state.subtitle)\n                        .font(.subheadline)\n                        .lineLimit(1)\n                        .foregroundStyle(.secondary)\n                    HStack(spacing: 8) {\n                        Circle()\n                            .fill(getStatusDotColor(state.status))\n                            .frame(width: 6, height: 6)\n                        Text(localizedStatus(state.status))\n                            .font(.caption)\n                            .foregroundStyle(.secondary)\n                    }\n                }\n                Spacer(minLength: 8)\n                Image(systemName: state.hasTerminal ? \"terminal\" : \"bolt.horizontal.circle\")\n                    .font(.title3)\n                    .foregroundStyle(.secondary)\n            }\n            .padding(.horizontal)\n            .padding(.vertical, 10)\n        } dynamicIsland: { context in\n            DynamicIsland {\n                DynamicIslandExpandedRegion(.leading) {\n                    VStack(alignment: .leading, spacing: 4) {\n                        HStack(spacing: 4) {\n                            Text(context.state.hasTerminal ? NSLocalizedString(\"Terminal\", comment: \"Terminal label\") : \"SSH\")\n                                .font(.caption2)\n                                .foregroundStyle(.secondary)\n                            if context.state.connectionCount > 1 {\n                                Text(\"(\\(context.state.connectionCount))\")\n                                    .font(.caption2)\n                                    .foregroundStyle(.secondary)\n                            }\n                        }\n                        Text(context.state.title)\n                            .font(.subheadline)\n                            .lineLimit(1)\n                            .truncationMode(.tail)\n                    }\n                    .padding(.vertical, 8)\n                    .padding(.horizontal, 8)\n                }\n                DynamicIslandExpandedRegion(.trailing) {\n                    VStack(alignment: .trailing, spacing: 6) {\n                        HStack(spacing: 6) {\n                            Circle()\n                                .fill(getStatusDotColor(context.state.status))\n                                .frame(width: 6, height: 6)\n                            Text(localizedStatus(context.state.status))\n                                .font(.caption2)\n                                .foregroundStyle(.secondary)\n                        }\n                    }\n                    .padding(.vertical, 8)\n                    .padding(.horizontal, 8)\n                }\n                DynamicIslandExpandedRegion(.bottom) {\n                    Text(context.state.subtitle)\n                        .font(.caption)\n                        .lineLimit(1)\n                        .foregroundStyle(.secondary)\n                }\n            } compactLeading: {\n                Image(systemName: context.state.hasTerminal ? \"terminal\" : \"bolt.horizontal.circle\")\n            } compactTrailing: {\n                EmptyView()\n            } minimal: {\n                Image(systemName: context.state.hasTerminal ? \"terminal\" : \"bolt.horizontal.circle\")\n            }\n        }\n    }\n}\n\n#if DEBUG\n@available(iOS 16.2, *)\nstruct TerminalLiveActivity_Previews: PreviewProvider {\n    static let attributes = TerminalAttributes(id: \"preview\")\n    static let contentState = TerminalAttributes.ContentState(\n        id: \"preview\",\n        title: \"root@server-01\",\n        subtitle: \"CPU 37% • Mem 1.3G/2.0G\",\n        status: \"Connected\",\n        startTime: Date().addingTimeInterval(-1234),\n        hasTerminal: true,\n        connectionCount: 2\n    )\n\n    static var previews: some View {\n        Group {\n            // 锁屏 / 通知样式预览\n            attributes\n                .previewContext(contentState, viewKind: .content)\n                .previewDisplayName(\"Lock Screen\")\n\n            // 岛屿展开态预览\n            attributes\n                .previewContext(contentState, viewKind: .dynamicIsland(.expanded))\n                .previewDisplayName(\"Dynamic Island • Expanded\")\n\n            // 岛屿紧凑态预览\n            attributes\n                .previewContext(contentState, viewKind: .dynamicIsland(.compact))\n                .previewDisplayName(\"Dynamic Island • Compact\")\n\n            // 岛屿最小态预览\n            attributes\n                .previewContext(contentState, viewKind: .dynamicIsland(.minimal))\n                .previewDisplayName(\"Dynamic Island • Minimal\")\n        }\n    }\n}\n#endif\n"
  },
  {
    "path": "ios/StatusWidget/TerminalLiveActivityAttributes.swift",
    "content": "//\n//  TerminalLiveActivityAttributes.swift\n//  StatusWidget\n//\n//  Defines ActivityKit attributes and content state for SSH/Terminal Live Activities.\n//\n\nimport Foundation\nimport ActivityKit\n\n@available(iOS 16.1, *)\npublic struct TerminalAttributes: ActivityAttributes {\n    public struct ContentState: Codable, Hashable {\n        public var id: String\n        public var title: String\n        public var subtitle: String\n        public var status: String\n        public var startTime: Date\n        public var hasTerminal: Bool\n        public var connectionCount: Int\n\n        public init(id: String, title: String, subtitle: String, status: String, startTime: Date, hasTerminal: Bool, connectionCount: Int = 1) {\n            self.id = id\n            self.title = title\n            self.subtitle = subtitle\n            self.status = status\n            self.startTime = startTime\n            self.hasTerminal = hasTerminal\n            self.connectionCount = connectionCount\n        }\n    }\n\n    public var id: String\n\n    public init(id: String) {\n        self.id = id\n    }\n}\n\n"
  },
  {
    "path": "ios/StatusWidget/de.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Verbunden\";\n\"Connecting\" = \"Verbindung wird hergestellt\";\n\"Disconnected\" = \"Getrennt\";\n\"Multiple SSH sessions active\" = \"Mehrere aktive SSH-Sitzungen\";\n\"1 connection\" = \"1 Verbindung\";\n\"%d connections\" = \"%d Verbindungen\";\n\n"
  },
  {
    "path": "ios/StatusWidget/de.lproj/StatusWidget.strings",
    "content": "\"5QiOUM\" = \"Url\";\n\n\"gpCwrM\" = \"Status-Widget-Konfiguration\";\n\n\"j41owf\" = \"Überwachungs-URL\";\n\n\"tVvJ9c\" = \"Desktop-Widget-Konfiguration\";\n\n\"wwvqC2\" = \"https://\";\n\n"
  },
  {
    "path": "ios/StatusWidget/en.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Connected\";\n\"Connecting\" = \"Connecting\";\n\"Disconnected\" = \"Disconnected\";\n\"Multiple SSH sessions active\" = \"Multiple SSH sessions active\";\n\"1 connection\" = \"1 connection\";\n\"%d connections\" = \"%d connections\";\n\n"
  },
  {
    "path": "ios/StatusWidget/en.lproj/StatusWidget.strings",
    "content": "\"5QiOUM\" = \"Url\";\n\n\"gpCwrM\" = \"Status Widget Config\";\n\n\"j41owf\" = \"Monitoring URL\";\n\n\"tVvJ9c\" = \"Desktop widget configuration\";\n\n\"wwvqC2\" = \"https://\";\n\n"
  },
  {
    "path": "ios/StatusWidget/es.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Conectado\";\n\"Connecting\" = \"Conectando\";\n\"Disconnected\" = \"Desconectado\";\n\"Multiple SSH sessions active\" = \"Varias sesiones SSH activas\";\n\"1 connection\" = \"1 conexión\";\n\"%d connections\" = \"%d conexiones\";\n\n"
  },
  {
    "path": "ios/StatusWidget/es.lproj/StatusWidget.strings",
    "content": "\"5QiOUM\" = \"Url\";\n\n\"gpCwrM\" = \"Configuración del widget de estado\";\n\n\"j41owf\" = \"URL de monitoreo\";\n\n\"tVvJ9c\" = \"Configuración del widget de escritorio\";\n\n\"wwvqC2\" = \"https://\";\n\n"
  },
  {
    "path": "ios/StatusWidget/fr.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Connecté\";\n\"Connecting\" = \"Connexion en cours\";\n\"Disconnected\" = \"Déconnecté\";\n\"Multiple SSH sessions active\" = \"Plusieurs sessions SSH actives\";\n\"1 connection\" = \"1 connexion\";\n\"%d connections\" = \"%d connexions\";\n\n"
  },
  {
    "path": "ios/StatusWidget/fr.lproj/StatusWidget.strings",
    "content": "\"5QiOUM\" = \"Url\";\n\n\"gpCwrM\" = \"Configuration du widget d'état\";\n\n\"j41owf\" = \"URL de surveillance\";\n\n\"tVvJ9c\" = \"Configuration du widget de bureau\";\n\n\"wwvqC2\" = \"https://\";\n\n"
  },
  {
    "path": "ios/StatusWidget/id.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Terhubung\";\n\"Connecting\" = \"Menghubungkan\";\n\"Disconnected\" = \"Terputus\";\n\"Multiple SSH sessions active\" = \"Beberapa sesi SSH aktif\";\n\"1 connection\" = \"1 koneksi\";\n\"%d connections\" = \"%d koneksi\";\n\n"
  },
  {
    "path": "ios/StatusWidget/id.lproj/StatusWidget.strings",
    "content": "\"5QiOUM\" = \"Url\";\n\n\"gpCwrM\" = \"Konfigurasi Widget Status\";\n\n\"j41owf\" = \"URL pemantauan\";\n\n\"tVvJ9c\" = \"Konfigurasi widget desktop\";\n\n\"wwvqC2\" = \"https://\";\n\n"
  },
  {
    "path": "ios/StatusWidget/ja.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"ターミナル\";\n\"Connected\" = \"接続済み\";\n\"Connecting\" = \"接続中\";\n\"Disconnected\" = \"切断\";\n\"Multiple SSH sessions active\" = \"複数の SSH セッションがアクティブ\";\n\"1 connection\" = \"1 件の接続\";\n\"%d connections\" = \"%d 件の接続\";\n\n"
  },
  {
    "path": "ios/StatusWidget/ja.lproj/StatusWidget.strings",
    "content": "\"5QiOUM\" = \"Url\";\n\n\"gpCwrM\" = \"ステータスウィジェットの設定\";\n\n\"j41owf\" = \"監視URL\";\n\n\"tVvJ9c\" = \"デスクトップウィジェットの設定\";\n\n\"wwvqC2\" = \"https://\";\n\n"
  },
  {
    "path": "ios/StatusWidget/pt-BR.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Terminal\";\n\"Connected\" = \"Conectado\";\n\"Connecting\" = \"Conectando\";\n\"Disconnected\" = \"Desconectado\";\n\"Multiple SSH sessions active\" = \"Várias sessões SSH ativas\";\n\"1 connection\" = \"1 conexão\";\n\"%d connections\" = \"%d conexões\";\n\n"
  },
  {
    "path": "ios/StatusWidget/pt-BR.lproj/StatusWidget.strings",
    "content": "\"5QiOUM\" = \"Url\";\n\n\"gpCwrM\" = \"Configuração do Widget de Status\";\n\n\"j41owf\" = \"URL de monitoramento\";\n\n\"tVvJ9c\" = \"Configuração do widget de desktop\";\n\n\"wwvqC2\" = \"https://\";\n\n"
  },
  {
    "path": "ios/StatusWidget/ru.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"Терминал\";\n\"Connected\" = \"Подключено\";\n\"Connecting\" = \"Подключение\";\n\"Disconnected\" = \"Отключено\";\n\"Multiple SSH sessions active\" = \"Несколько активных сеансов SSH\";\n\"1 connection\" = \"1 подключение\";\n\"%d connections\" = \"%d подключений\";\n\n"
  },
  {
    "path": "ios/StatusWidget/ru.lproj/StatusWidget.strings",
    "content": "\"5QiOUM\" = \"Url\";\n\n\"gpCwrM\" = \"Конфигурация виджета статуса\";\n\n\"j41owf\" = \"URL мониторинга\";\n\n\"tVvJ9c\" = \"Конфигурация виджета рабочего стола\";\n\n\"wwvqC2\" = \"https://\";\n\n"
  },
  {
    "path": "ios/StatusWidget/zh-Hans.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"终端\";\n\"Connected\" = \"已连接\";\n\"Connecting\" = \"连接中\";\n\"Disconnected\" = \"已断开连接\";\n\"Multiple SSH sessions active\" = \"多个 SSH 会话正在活动\";\n\"1 connection\" = \"1 个连接\";\n\"%d connections\" = \"%d 个连接\";\n\n"
  },
  {
    "path": "ios/StatusWidget/zh-Hans.lproj/StatusWidget.strings",
    "content": "\"5QiOUM\" = \"Url\";\n\n\"gpCwrM\" = \"状态小部件配置\";\n\n\"j41owf\" = \"监控URL\";\n\n\"tVvJ9c\" = \"桌面小部件配置\";\n\n\"wwvqC2\" = \"https://\";\n\n"
  },
  {
    "path": "ios/StatusWidget/zh-Hant.lproj/Localizable.strings",
    "content": "\"Terminal\" = \"終端機\";\n\"Connected\" = \"已連線\";\n\"Connecting\" = \"連線中\";\n\"Disconnected\" = \"已中斷連線\";\n\"Multiple SSH sessions active\" = \"多個 SSH 連線運行中\";\n\"1 connection\" = \"1 個連線\";\n\"%d connections\" = \"%d 個連線\";\n\n"
  },
  {
    "path": "ios/StatusWidget/zh-Hant.lproj/StatusWidget.strings",
    "content": "\"5QiOUM\" = \"Url\";\n\n\"gpCwrM\" = \"狀態小工具配置\";\n\n\"j41owf\" = \"監控URL\";\n\n\"tVvJ9c\" = \"桌面小部件配置\";\n\n\"wwvqC2\" = \"https://\";\n\n"
  },
  {
    "path": "ios/WatchApp/ContentView.swift",
    "content": "//\n//  ContentView.swift\n//  WatchEnd Watch App\n//\n//  Created by lolli on 2023/9/16.\n//\n\nimport SwiftUI\n\nstruct ContentView: View {\n    @ObservedObject var _mgr = PhoneConnMgr()\n    @State private var selection: Int = 0\n    @State private var refreshAllCounter: Int = 0\n    \n    var body: some View {\n        let hasServers = !_mgr.urls.isEmpty\n        let pagesCount = hasServers ? _mgr.urls.count : 1\n        TabView(selection: $selection) {\n            ForEach(0 ..< pagesCount, id:\\.self) { index in\n                let url = hasServers ? _mgr.urls[index] : nil\n                PageView(\n                    url: url,\n                    state: .loading,\n                    refreshAllCounter: refreshAllCounter,\n                    onRefreshAll: { refreshAllCounter += 1 }\n                )\n                .tag(index)\n            }\n        }\n        .tabViewStyle(PageTabViewStyle())\n        // 当 URL 列表变化时，尽量保持当前选中的页面不变\n        .onChange(of: _mgr.urls) { newValue in\n            let newCount = newValue.count\n            // 当没有服务器时，只有占位页\n            if newCount == 0 {\n                selection = 0\n            } else if selection >= newCount {\n                // 如果当前选择超出范围，则跳到最后一个有效页\n                selection = max(0, newCount - 1)\n            }\n        }\n        // 持久化当前选择，供 Widget 使用\n        .onChange(of: selection) { newIndex in\n            let appGroupId = \"group.com.lollipopkit.toolbox\"\n            if let defaults = UserDefaults(suiteName: appGroupId) {\n                defaults.set(newIndex, forKey: \"watch_shared_selected_index\")\n            }\n        }\n        .onAppear {\n            // 尽量恢复上一次的选择\n            let appGroupId = \"group.com.lollipopkit.toolbox\"\n            let saved = UserDefaults(suiteName: appGroupId)?.integer(forKey: \"watch_shared_selected_index\") ?? 0\n            if !_mgr.urls.isEmpty {\n                selection = min(max(0, saved), _mgr.urls.count - 1)\n            } else {\n                selection = 0\n            }\n        }\n    }\n}\n\nstruct PageView: View {\n    let url: String?\n    @State var state: ContentState\n    // 触发所有页面刷新的计数器\n    let refreshAllCounter: Int\n    let onRefreshAll: () -> Void\n    \n    var body: some View {\n        if url == nil {\n            VStack {\n                Spacer()\n                Image(systemName: \"exclamationmark.triangle.fill\")\n                Spacer()\n                Text(\"Tip: Config it in the iOS app settings.\").font(.system(.body, design: .monospaced)).padding(.horizontal, 7)\n                Spacer()\n            }\n        } else {\n            Group {\n                switch state {\n                case .loading:\n                    ProgressView().padding().onAppear {\n                        getStatus(url: url!)\n                    }\n                case .error(let err):\n                switch err {\n                case .http(let description):\n                    VStack(alignment: .center) {\n                        Text(description)\n                        HStack(spacing: 10) {\n                            Button(action: {\n                                state = .loading\n                            }){\n                                Image(systemName: \"arrow.clockwise\")\n                            }.buttonStyle(.plain)\n                            Button(action: {\n                                onRefreshAll()\n                            }){\n                                Image(systemName: \"arrow.triangle.2.circlepath\")\n                            }.buttonStyle(.plain)\n                        }\n                    }\n                case .url(_):\n                    Link(\"View help\", destination: helpUrl)\n                }\n                case .normal(let status):\n                    VStack(alignment: .leading) {\n                    HStack {\n                        Text(status.name).font(.system(.title, design: .monospaced))\n                        Spacer()\n                        HStack(spacing: 10) {\n                            Button(action: {\n                                state = .loading\n                            }){\n                                Image(systemName: \"arrow.clockwise\")\n                            }.buttonStyle(.plain)\n                            Button(action: {\n                                onRefreshAll()\n                            }){\n                                Image(systemName: \"arrow.triangle.2.circlepath\")\n                            }.buttonStyle(.plain)\n                        }\n                    }\n                    Spacer()\n                    DetailItem(icon: \"cpu\", text: status.cpu)\n                    DetailItem(icon: \"memorychip\", text: status.mem)\n                    DetailItem(icon: \"externaldrive\", text: status.disk)\n                    DetailItem(icon: \"network\", text: status.net)\n                }.frame(maxWidth: .infinity, maxHeight: .infinity).padding([.horizontal], 11)\n                }\n            }\n            .onChange(of: refreshAllCounter) { _ in\n                if let url = url {\n                    getStatus(url: url)\n                }\n            }\n        }\n    }\n    \n    func getStatus(url: String) {\n        state = .loading\n        if url.count < 12 {\n            state = .error(.url(\"url len < 12\"))\n            return\n        }\n        guard let url = URL(string: url) else {\n            state = .error(.url(\"parse url failed\"))\n            return\n        }\n        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in\n            // 所有 UI 状态更新必须在主线程执行，否则可能导致 TabView 跳回第一页等问题\n            func setStateOnMain(_ newState: ContentState) {\n                DispatchQueue.main.async {\n                    self.state = newState\n                }\n            }\n\n            if let error = error {\n                setStateOnMain(.error(.http(error.localizedDescription)))\n                return\n            }\n            guard let data = data else {\n                setStateOnMain(.error(.http(\"empty data\")))\n                return\n            }\n            guard let jsonAll = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {\n                setStateOnMain(.error(.http(\"json parse fail\")))\n                return\n            }\n            guard let code = jsonAll[\"code\"] as? Int else {\n                setStateOnMain(.error(.http(\"code is nil\")))\n                return\n            }\n            if (code != 0) {\n                let msg = jsonAll[\"msg\"] as? String ?? \"\"\n                setStateOnMain(.error(.http(msg)))\n                return\n            }\n\n            let json = jsonAll[\"data\"] as? [String: Any] ?? [:]\n            let name = json[\"name\"] as? String ?? \"\"\n            let disk = json[\"disk\"] as? String ?? \"\"\n            let cpu = json[\"cpu\"] as? String ?? \"\"\n            let mem = json[\"mem\"] as? String ?? \"\"\n            let net = json[\"net\"] as? String ?? \"\"\n            let status = Status(name: name, cpu: cpu, mem: mem, disk: disk, net: net)\n            setStateOnMain(.normal(status))\n            // 将最新数据写入 App Group，供表盘/叠放的 Widget 使用\n            let appGroupId = \"group.com.lollipopkit.toolbox\"\n            if let defaults = UserDefaults(suiteName: appGroupId) {\n                var statusMap = (defaults.dictionary(forKey: \"watch_shared_status_by_url\") as? [String: [String: String]]) ?? [:]\n                statusMap[url.absoluteString] = [\n                    \"name\": status.name,\n                    \"cpu\": status.cpu,\n                    \"mem\": status.mem,\n                    \"disk\": status.disk,\n                    \"net\": status.net\n                ]\n                defaults.set(statusMap, forKey: \"watch_shared_status_by_url\")\n            }\n        }\n        task.resume()\n    }\n    \n    // 监听“刷新全部”触发器变化，主动刷新当前页\n    @ViewBuilder\n    var _onRefreshAllHook: some View {\n        EmptyView()\n            .onChange(of: refreshAllCounter) { _ in\n                if let url = url {\n                    getStatus(url: url)\n                }\n            }\n    }\n}\n\nstruct ContentView_Previews: PreviewProvider {\n    static var previews: some View {\n        ContentView()\n    }\n}\n\nstruct DetailItem: View {\n    let icon: String\n    let text: String\n    \n    var body: some View {\n        HStack(spacing: 5.7) {\n            Image(systemName: icon).resizable().foregroundColor(.white).frame(width: 11, height: 11, alignment: .center)\n            Text(text)\n                .font(.system(.caption2, design: .monospaced))\n                .foregroundColor(.white)\n        }\n    }\n}\n"
  },
  {
    "path": "ios/WatchApp/PhoneConnMgr.swift",
    "content": "//\n//  PhoneConnMgr.swift\n//  WatchEnd Watch App\n//\n//  Created by lolli on 2023/9/16.\n//\n\nimport WatchConnectivity\n\nclass PhoneConnMgr: NSObject, WCSessionDelegate, ObservableObject {\n    var session: WCSession?\n    var _ctx: [String: Any] = [:]\n    var ctx: [String: Any] {\n        set {\n            Store.setCtx(newValue)\n            updateUrls(newValue)\n\n            // Notify the view to update, but the [urls] are already published\n            // so the view will automatically update when [urls] changes.\n            // DispatchQueue.main.async {\n            //     self.objectWillChange.send()\n            // }\n        }\n        get {\n            return _ctx\n        }\n    }\n    var userInfo: [String: Any] = [:]\n    @Published var urls: [String] = []\n\n    override init() {\n        super.init()\n        if !WCSession.isSupported() {\n            print(\"WCSession not supported\")\n        }\n        session = WCSession.default\n        session?.delegate = self\n        session?.activate()\n\n        _ctx = Store.getCtx()\n        updateUrls(_ctx)\n    }\n\n    func updateUrls(_ val: [String: Any]) {\n        if let urls = val[\"urls\"] as? [String] {\n            DispatchQueue.main.async {\n                let list = urls.filter { !$0.isEmpty }\n                self.urls = list\n                // Save URLs to App Group for widget access\n                let appGroupId = \"group.com.lollipopkit.toolbox\"\n                if let defaults = UserDefaults(suiteName: appGroupId) {\n                    defaults.set(list, forKey: \"watch_shared_urls\")\n                }\n            }\n        }\n    }\n\n    func session(\n        _ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState,\n        error: Error?\n    ) {\n        // Request latest data when the session is activated\n        if activationState == .activated {\n            requestLatestData()\n        }\n    }\n\n    // Receive realtime msgs\n    func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {\n        DispatchQueue.main.async {\n            self.ctx = message\n        }\n    }\n\n    // Receive UserInfo\n    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {\n        DispatchQueue.main.async {\n            self.ctx = userInfo\n        }\n    }\n\n    // Receive Application Context\n    func session(\n        _ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]\n    ) {\n        DispatchQueue.main.async {\n            self.ctx = applicationContext\n        }\n    }\n\n    private func requestLatestData(timeout: TimeInterval = 5.0, maxRetries: Int = 1) {\n        guard let session = session, session.isReachable else { return }\n\n        var didReceiveResponse = false\n        var retries = 0\n\n        func sendRequest() {\n            session.sendMessage([\"action\": \"requestData\"]) { response in\n                didReceiveResponse = true\n                DispatchQueue.main.async {\n                    self.ctx = response\n                }\n            } errorHandler: { error in\n                print(\"Request data failed: \\(error)\")\n                // Optionally, handle error UI here\n            }\n\n            // Timeout handling\n            DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { [weak self] in\n                guard let self = self else { return }\n                if !didReceiveResponse {\n                    if retries < maxRetries {\n                        retries += 1\n                        print(\"No response, retrying requestLatestData (\\(retries))...\")\n                        sendRequest()\n                    } else {\n                        print(\"Request data timed out after \\(retries + 1) attempts.\")\n                        // Optionally, update UI to indicate timeout\n                    }\n                }\n            }\n        }\n\n        sendRequest()\n    }\n}\n"
  },
  {
    "path": "ios/WatchApp/PrivacyInfo.xcprivacy",
    "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>NSPrivacyAccessedAPITypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>NSPrivacyAccessedAPITypeReasons</key>\n\t\t\t<array>\n\t\t\t\t<string>CA92.1</string>\n\t\t\t</array>\n\t\t\t<key>NSPrivacyAccessedAPIType</key>\n\t\t\t<string>NSPrivacyAccessedAPICategoryUserDefaults</string>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/WatchApp/Store.swift",
    "content": "//\n//  Store.swift\n//  WatchEnd Watch App\n//\n//  Created by lolli on 2023/9/16.\n//\n\nimport Foundation\n\nclass Store {\n    static let defaults = UserDefaults.standard\n\n    static let _ctxKey = \"ctx\"\n    static func getCtx() -> [String: Any] {\n        return defaults.object(forKey: _ctxKey) as? [String: Any] ?? [:]\n    }\n    static func setCtx(_ ctx: [String: Any]) {\n        defaults.set(ctx, forKey: _ctxKey)\n    }\n}\n"
  },
  {
    "path": "ios/WatchApp/Watch.xcassets/AccentColor.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/WatchApp/Watch.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"AppIcon-1024.0x1024.0@1x.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"watchos\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/WatchApp/Watch.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/WatchApp/WatchEndApp.swift",
    "content": "//\n//  WatchEndApp.swift\n//  WatchEnd Watch App\n//\n//  Created by lolli on 2023/9/16.\n//\n\nimport SwiftUI\n\n@main\nstruct WatchEndApp: App {\n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n        }\n    }\n}\n"
  },
  {
    "path": "ios/WatchWidget/WatchStatusWidget.swift",
    "content": "//\n//  WatchStatusWidget.swift\n//  WatchStatusWidget Extension\n//\n//  Created by AI Assistant\n//\n\nimport WidgetKit\nimport SwiftUI\nimport Foundation\n\n// Simple model, independent from Runner target\nstruct Status: Hashable {\n    let name: String\n    let cpu: String\n    let mem: String\n    let disk: String\n    let net: String\n}\n\nstruct WatchProvider: TimelineProvider {\n    func placeholder(in context: Context) -> WatchEntry {\n        WatchEntry(date: Date(), status: Status(name: \"Server\", cpu: \"32%\", mem: \"1.3g/1.9g\", disk: \"7.1g/30g\", net: \"712k/1.2m\"))\n    }\n\n    func getSnapshot(in context: Context, completion: @escaping (WatchEntry) -> Void) {\n        completion(loadEntry())\n    }\n\n    func getTimeline(in context: Context, completion: @escaping (Timeline<WatchEntry>) -> Void) {\n        let entry = loadEntry()\n        let next = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) ?? Date().addingTimeInterval(900)\n        completion(Timeline(entries: [entry], policy: .after(next)))\n    }\n\n    private func loadEntry() -> WatchEntry {\n        let appGroupId = \"group.com.lollipopkit.toolbox\"\n        guard let defaults = UserDefaults(suiteName: appGroupId) else {\n            return WatchEntry(date: Date(), status: Status(name: \"Server\", cpu: \"--%\", mem: \"-\", disk: \"-\", net: \"-\"))\n        }\n        \n        let urls = (defaults.array(forKey: \"watch_shared_urls\") as? [String]) ?? []\n        let idx = defaults.integer(forKey: \"watch_shared_selected_index\")\n        var status: Status? = nil\n        \n        if !urls.isEmpty {\n            let i = min(max(0, idx), urls.count - 1)\n            let url = urls[i]\n            \n            // Load status from shared defaults\n            if let statusMap = defaults.dictionary(forKey: \"watch_shared_status_by_url\") as? [String: [String: String]],\n               let statusDict = statusMap[url] {\n                status = Status(\n                    name: statusDict[\"name\"] ?? \"\",\n                    cpu: statusDict[\"cpu\"] ?? \"\",\n                    mem: statusDict[\"mem\"] ?? \"\",\n                    disk: statusDict[\"disk\"] ?? \"\",\n                    net: statusDict[\"net\"] ?? \"\"\n                )\n            }\n        }\n        return WatchEntry(\n            date: Date(),\n            status: status ?? Status(name: \"Server\", cpu: \"--%\", mem: \"-\", disk: \"-\", net: \"-\")\n        )\n    }\n}\n\nstruct WatchEntry: TimelineEntry {\n    let date: Date\n    let status: Status\n}\n\nstruct WatchStatusWidgetEntryView: View {\n    var entry: WatchProvider.Entry\n\n    @Environment(\\.widgetFamily) var family\n\n    var body: some View {\n        switch family {\n        case .accessoryCircular:\n            ZStack {\n                Circle().stroke(Color.primary.opacity(0.15), lineWidth: 4)\n                CirclePercent(percent: entry.status.cpu)\n                Text(entry.status.cpu.replacingOccurrences(of: \"%\", with: \"\")).font(.system(size: 10, weight: .bold, design: .monospaced))\n            }\n            .padding(2)\n        case .accessoryRectangular:\n            VStack(alignment: .leading, spacing: 2) {\n                HStack {\n                    Text(entry.status.name).font(.system(size: 12, weight: .semibold, design: .monospaced))\n                    Spacer()\n                }\n                HStack(spacing: 6) {\n                    Label(entry.status.cpu, systemImage: \"cpu\").font(.system(size: 11, design: .monospaced))\n                }\n            }\n        case .accessoryInline:\n            Text(\"\\(entry.status.name) \\(entry.status.cpu)\")\n        default:\n            VStack {\n                Text(entry.status.name)\n                Text(entry.status.cpu)\n            }\n        }\n    }\n}\n\nstruct WatchStatusWidget: Widget {\n    let kind: String = \"WatchStatusWidget\"\n\n    var body: some WidgetConfiguration {\n        StaticConfiguration(kind: kind, provider: WatchProvider()) { entry in\n            WatchStatusWidgetEntryView(entry: entry)\n        }\n        .configurationDisplayName(\"Server Status\")\n        .description(\"Shows the selected server status.\")\n        .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])\n    }\n}\n\nstruct WatchStatusWidget_Previews: PreviewProvider {\n    static var previews: some View {\n        WatchStatusWidgetEntryView(entry: WatchEntry(date: Date(), status: Status(name: \"Server\", cpu: \"37%\", mem: \"1.3g/1.9g\", disk: \"7.1g/30g\", net: \"712k/1.2m\")))\n            .previewContext(WidgetPreviewContext(family: .accessoryRectangular))\n    }\n}\n\n// Helpers reused from iOS widget with lightweight versions\nstruct CirclePercent: View {\n    let percent: String\n    var body: some View {\n        let percentD = Double(percent.trimmingCharacters(in: .init(charactersIn: \"% \"))) ?? 0\n        let p = max(0, min(100, percentD)) / 100.0\n        Circle()\n            .trim(from: 0, to: CGFloat(p))\n            .stroke(Color.primary, style: StrokeStyle(lineWidth: 4, lineCap: .round))\n            .rotationEffect(.degrees(-90))\n    }\n}\n\n"
  },
  {
    "path": "ios/WatchWidget/WatchStatusWidgetBundle.swift",
    "content": "//\n//  WatchStatusWidgetBundle.swift\n//  WatchStatusWidget Extension\n//\n//  Created by AI Assistant\n//\n\nimport WidgetKit\nimport SwiftUI\n\n@main\nstruct WatchStatusWidgetBundle: WidgetBundle {\n    var body: some Widget {\n        WatchStatusWidget()\n    }\n}\n\n"
  },
  {
    "path": "l10n.yaml",
    "content": "arb-dir: lib/l10n\ntemplate-arb-file: app_en.arb\noutput-localization-file: l10n.dart\noutput-dir: lib/generated/l10n\nuntranslated-messages-file: untranlated.json"
  },
  {
    "path": "lib/app.dart",
    "content": "import 'package:dynamic_color/dynamic_color.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:fl_lib/generated/l10n/lib_l10n.dart';\nimport 'package:flutter/material.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/core/app_navigator.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/res/build_data.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/generated/l10n/l10n.dart';\nimport 'package:server_box/view/page/home.dart';\n\npart 'intro.dart';\n\nclass MyApp extends StatefulWidget {\n  const MyApp({super.key});\n\n  @override\n  State<MyApp> createState() => _MyAppState();\n}\n\nclass _MyAppState extends State<MyApp> {\n  late final Future<List<IntroPageBuilder>> _introFuture = _IntroPage.builders;\n\n  @override\n  Widget build(BuildContext context) {\n    _setup(context);\n\n    return ListenableBuilder(\n      listenable: RNodes.app,\n      builder: (context, _) {\n        if (!Stores.setting.useSystemPrimaryColor.fetch()) {\n          return _build(context);\n        }\n\n        return _buildDynamicColor(context);\n      },\n    );\n  }\n\n  Widget _build(BuildContext context) {\n    final colorSeed = Color(Stores.setting.colorSeed.fetch());\n\n    UIs.colorSeed = colorSeed;\n    UIs.primaryColor = colorSeed;\n\n    return _buildApp(\n      context,\n      light: ThemeData(\n        useMaterial3: true,\n        colorSchemeSeed: UIs.colorSeed,\n        appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),\n      ),\n      dark: ThemeData(\n        useMaterial3: true,\n        brightness: Brightness.dark,\n        colorSchemeSeed: UIs.colorSeed,\n        appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),\n      ),\n    );\n  }\n\n  Widget _buildDynamicColor(BuildContext context) {\n    return DynamicColorBuilder(\n      builder: (light, dark) {\n        final lightSeed = light?.primary;\n        final darkSeed = dark?.primary;\n\n        final lightTheme = ThemeData(\n          useMaterial3: true,\n          colorSchemeSeed: lightSeed,\n          appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),\n        );\n        final darkTheme = ThemeData(\n          useMaterial3: true,\n          brightness: Brightness.dark,\n          colorSchemeSeed: darkSeed,\n          appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),\n        );\n\n        if (context.isDark && dark != null) {\n          UIs.primaryColor = dark.primary;\n          UIs.colorSeed = dark.primary;\n        } else if (!context.isDark && light != null) {\n          UIs.primaryColor = light.primary;\n          UIs.colorSeed = light.primary;\n        } else {\n          final fallbackColor = Color(Stores.setting.colorSeed.fetch());\n          UIs.primaryColor = fallbackColor;\n          UIs.colorSeed = fallbackColor;\n        }\n\n        return _buildApp(context, light: lightTheme, dark: darkTheme);\n      },\n    );\n  }\n\n  Widget _buildApp(BuildContext ctx, {required ThemeData light, required ThemeData dark}) {\n    final tMode = Stores.setting.themeMode.fetch();\n    // Issue #57\n    final themeMode = switch (tMode) {\n      1 || 2 => ThemeMode.values[tMode],\n      3 => ThemeMode.dark,\n      _ => ThemeMode.system,\n    };\n    final locale = Stores.setting.locale.fetch().toLocale;\n\n    return MaterialApp(\n      key: ValueKey(locale),\n      navigatorKey: AppNavigator.key,\n      builder: ResponsivePoints.builder,\n      locale: locale,\n      localizationsDelegates: const [LibLocalizations.delegate, ...AppLocalizations.localizationsDelegates],\n      supportedLocales: AppLocalizations.supportedLocales,\n      localeListResolutionCallback: LocaleUtil.resolve,\n      navigatorObservers: [AppRouteObserver.instance],\n      title: BuildData.name,\n      themeMode: themeMode,\n      theme: light.fixWindowsFont,\n      darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,\n      home: FutureBuilder<List<IntroPageBuilder>>(\n        future: _introFuture,\n        builder: (context, snapshot) {\n          context.setLibL10n();\n          final appL10n = AppLocalizations.of(context);\n          if (appL10n != null) l10n = appL10n;\n\n          Widget child;\n          if (snapshot.connectionState == ConnectionState.waiting) {\n            child = const Scaffold(body: Center(child: CircularProgressIndicator()));\n          } else {\n            final intros = snapshot.data ?? [];\n            if (intros.isNotEmpty) {\n              child = _IntroPage(intros);\n            } else {\n              child = const HomePage();\n            }\n          }\n\n          return VirtualWindowFrame(title: BuildData.name, child: child);\n        },\n      ),\n    );\n  }\n}\n\nvoid _setup(BuildContext context) async {\n  SystemUIs.setTransparentNavigationBar(context);\n}\n"
  },
  {
    "path": "lib/core/app_navigator.dart",
    "content": "import 'package:flutter/widgets.dart';\n\n/// Global navigator access used for cross-cutting flows (e.g. dialogs).\nabstract final class AppNavigator {\n  static final key = GlobalKey<NavigatorState>();\n\n  static BuildContext? get context => key.currentContext;\n}\n"
  },
  {
    "path": "lib/core/chan.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/services.dart';\nimport 'package:server_box/data/res/misc.dart';\nimport 'package:server_box/data/res/store.dart';\n\nabstract final class MethodChans {\n  static const _channel = MethodChannel('${Miscs.pkgName}/main_chan');\n\n  static void moveToBg() {\n    _channel.invokeMethod('sendToBackground');\n  }\n\n  /// Issue #662\n  static void startService() {\n    if (Stores.setting.fgService.fetch() != true) return;\n    _channel.invokeMethod('startService');\n  }\n\n  /// Issue #662\n  static void stopService() {\n    if (Stores.setting.fgService.fetch() != true) return;\n    _channel.invokeMethod('stopService');\n  }\n\n  static void updateHomeWidget() async {\n    if (!isIOS && !isAndroid) return;\n    if (!Stores.setting.autoUpdateHomeWidget.fetch()) return;\n    await _channel.invokeMethod('updateHomeWidget');\n  }\n\n  /// Update Android foreground service notifications for SSH sessions\n  /// The [payload] is a JSON string describing sessions list.\n  static Future<void> updateSessions(String payload) async {\n    if (!isAndroid) return;\n    try {\n      Loggers.app.info('Updating Android sessions: $payload');\n      await _channel.invokeMethod('updateSessions', payload);\n    } catch (e, s) {\n      Loggers.app.warning('Failed to update Android sessions', e, s);\n    }\n  }\n\n  /// Query whether the Android foreground service is currently running.\n  static Future<bool> isServiceRunning() async {\n    if (!isAndroid) return false;\n    try {\n      final res = await _channel.invokeMethod('isServiceRunning');\n      return res == true;\n    } catch (e, s) {\n      Loggers.app.warning('Failed to check if Android service is running', e, s);\n      return false;\n    }\n  }\n\n  // iOS Live Activities controls\n  static Future<void> startLiveActivity(String payload) async {\n    if (!isIOS) return;\n    try {\n      Loggers.app.info('Starting iOS Live Activity: $payload');\n      await _channel.invokeMethod('startLiveActivity', payload);\n    } catch (e, s) {\n      Loggers.app.warning('Failed to start iOS Live Activity', e, s);\n    }\n  }\n\n  static Future<void> updateLiveActivity(String payload) async {\n    if (!isIOS) return;\n    try {\n      Loggers.app.info('Updating iOS Live Activity: $payload');\n      await _channel.invokeMethod('updateLiveActivity', payload);\n    } catch (e, s) {\n      Loggers.app.warning('Failed to update iOS Live Activity', e, s);\n    }\n  }\n\n  static Future<void> stopLiveActivity() async {\n    if (!isIOS) return;\n    try {\n      Loggers.app.info('Stopping iOS Live Activity');\n      await _channel.invokeMethod('stopLiveActivity');\n    } catch (e, s) {\n      Loggers.app.warning('Failed to stop iOS Live Activity', e, s);\n    }\n  }\n\n  /// Register a handler for native -> Flutter callbacks.\n  /// Currently handles: \n  /// - `disconnectSession` with argument map {id: string}\n  /// - `stopAllConnections` with no arguments\n  static void registerHandler(Future<void> Function(String id) onDisconnect, [VoidCallback? onStopAll]) {\n    _channel.setMethodCallHandler((call) async {\n      switch (call.method) {\n        case 'disconnectSession':\n          final args = call.arguments;\n          final id = args is Map ? args['id'] as String? : args as String?;\n          if (id != null && id.isNotEmpty) {\n            await onDisconnect(id);\n          }\n          return;\n        case 'stopAllConnections':\n          onStopAll?.call();\n          return;\n        default:\n          return;\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "lib/core/extension/context/locale.dart",
    "content": "import 'package:flutter/widgets.dart';\nimport 'package:server_box/generated/l10n/l10n.dart';\nimport 'package:server_box/generated/l10n/l10n_en.dart';\n\nAppLocalizations l10n = AppLocalizationsEn();\n\nextension LocaleX on BuildContext {\n  AppLocalizations get l10n => AppLocalizations.of(this)!;\n}\n"
  },
  {
    "path": "lib/core/extension/server.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/data/model/app/scripts/cmd_types.dart';\nimport 'package:server_box/data/model/server/dist.dart';\nimport 'package:server_box/data/provider/server/single.dart';\nimport 'package:server_box/data/res/store.dart';\n\nextension LogoExt on ServerState {\n  String? getLogoUrl(BuildContext context) {\n    var logoUrl = spi.custom?.logoUrl ?? Stores.setting.serverLogoUrl.fetch().selfNotEmptyOrNull;\n    if (logoUrl == null) {\n      return null;\n    }\n    final dist = status.more[StatusCmdType.sys]?.dist;\n    if (dist != null) {\n      logoUrl = logoUrl.replaceFirst('{DIST}', dist.name);\n    }\n    logoUrl = logoUrl.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');\n    return logoUrl;\n  }\n}\n"
  },
  {
    "path": "lib/core/extension/sftpfile.dart",
    "content": "import 'package:dartssh2/dartssh2.dart';\nimport 'package:server_box/view/widget/unix_perm.dart';\n\nextension SftpFileX on SftpFileMode {\n  String get str {\n    final user = _getRoleMode(userRead, userWrite, userExecute);\n    final group = _getRoleMode(groupRead, groupWrite, groupExecute);\n    final other = _getRoleMode(otherRead, otherWrite, otherExecute);\n\n    return '$user$group$other';\n  }\n\n  UnixPerm toUnixPerm() {\n    return UnixPerm(\n      user: UnixPermOp(r: userRead, w: userWrite, x: userExecute),\n      group: UnixPermOp(r: groupRead, w: groupWrite, x: groupExecute),\n      other: UnixPermOp(r: otherRead, w: otherWrite, x: otherExecute),\n    );\n  }\n}\n\nString _getRoleMode(bool r, bool w, bool x) {\n  return '${r ? 'r' : '-'}${w ? 'w' : '-'}${x ? 'x' : '-'}';\n}\n"
  },
  {
    "path": "lib/core/extension/ssh_client.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:dartssh2/dartssh2.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:server_box/data/helper/ssh_decoder.dart';\nimport 'package:server_box/data/model/server/system.dart';\n\ntypedef OnStdout = void Function(String data, SSHSession session);\ntypedef OnStdin = void Function(SSHSession session);\n\nextension SSHClientX on SSHClient {\n  /// Create a persistent PowerShell session for Windows commands\n  Future<(SSHSession, String)> execPowerShell(\n    OnStdin onStdin, {\n    SSHPtyConfig? pty,\n    OnStdout? onStdout,\n    OnStdout? onStderr,\n    bool stdout = true,\n    bool stderr = true,\n    Map<String, String>? env,\n  }) async {\n    final session = await execute(\n      'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',\n      pty: pty,\n      environment: env,\n    );\n\n    final result = BytesBuilder(copy: false);\n    final stdoutDone = Completer<void>();\n    final stderrDone = Completer<void>();\n\n    session.stdout.listen(\n      (e) {\n        onStdout?.call(e.string, session);\n        if (stdout) result.add(e);\n      },\n      onDone: stdoutDone.complete,\n      onError: stderrDone.completeError,\n    );\n\n    session.stderr.listen(\n      (e) {\n        onStderr?.call(e.string, session);\n        // Don't add stderr to result, only stdout\n      },\n      onDone: stderrDone.complete,\n      onError: stderrDone.completeError,\n    );\n\n    onStdin(session);\n\n    await stdoutDone.future;\n    await stderrDone.future;\n\n    return (session, result.takeBytes().string);\n  }\n\n  Future<(SSHSession, String)> exec(\n    OnStdin onStdin, {\n    String? entry,\n    SSHPtyConfig? pty,\n    OnStdout? onStdout,\n    OnStdout? onStderr,\n    bool stdout = true,\n    bool stderr = true,\n    Map<String, String>? env,\n    SystemType? systemType,\n  }) async {\n    final session = await execute(\n      entry ??\n          switch (systemType) {\n            SystemType.windows => 'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',\n            _ => 'cat | sh',\n          },\n      pty: pty,\n      environment: env,\n    );\n\n    final result = BytesBuilder(copy: false);\n    final stdoutDone = Completer<void>();\n    final stderrDone = Completer<void>();\n\n    session.stdout.listen(\n      (e) {\n        onStdout?.call(e.string, session);\n        if (stdout) result.add(e);\n      },\n      onDone: stdoutDone.complete,\n      onError: stderrDone.completeError,\n    );\n\n    session.stderr.listen(\n      (e) {\n        onStderr?.call(e.string, session);\n        if (stderr) result.add(e);\n      },\n      onDone: stderrDone.complete,\n      onError: stderrDone.completeError,\n    );\n\n    onStdin(session);\n\n    await stdoutDone.future;\n    await stderrDone.future;\n\n    return (session, result.takeBytes().string);\n  }\n\n  /// Executes a command with password error detection.\n  ///\n  /// This method is used for executing commands where password has already been\n  /// handled beforehand (e.g., via base64 pipe in container commands).\n  /// It captures stderr via [onStderr] callback to detect sudo password errors\n  /// (e.g., \"Sorry, try again.\", \"incorrect password attempt\", or\n  /// \"a password is required\"), while excluding stderr from the returned\n  /// output via [stderr: false].\n  ///\n  /// Returns exitCode:\n  /// - 0: success\n  /// - 1: general error\n  /// - 2: sudo password error\n  Future<(int?, String)> execWithPwd(\n    String script, {\n    String? entry,\n    BuildContext? context,\n    OnStdout? onStdout,\n    OnStdout? onStderr,\n    required String id,\n  }) async {\n    var hasPasswordError = false;\n\n    final (session, output) = await exec(\n      (sess) {\n        sess.stdin.add('$script\\n'.uint8List);\n        sess.stdin.close();\n      },\n      onStderr: (data, session) async {\n        onStderr?.call(data, session);\n        if (data.contains('Sorry, try again.') ||\n            data.contains('incorrect password attempt') ||\n            data.contains('a password is required')) {\n          hasPasswordError = true;\n        }\n      },\n      onStdout: onStdout,\n      entry: entry,\n      stderr: false,\n    );\n\n    if (hasPasswordError) {\n      return (2, output);\n    }\n    return (session.exitCode, output);\n  }\n\n  Future<String> execForOutput(\n    String script, {\n    SSHPtyConfig? pty,\n    bool stdout = true,\n    bool stderr = true,\n    String? entry,\n    Map<String, String>? env,\n  }) async {\n    final ret = await exec(\n      (session) {\n        session.stdin.add('$script\\n'.uint8List);\n        session.stdin.close();\n      },\n      pty: pty,\n      env: env,\n      stdout: stdout,\n      stderr: stderr,\n      entry: entry,\n    );\n    return ret.$2;\n  }\n\n  /// Runs a command and decodes output safely with encoding fallback\n  ///\n  /// [systemType] - The system type (affects encoding choice)\n  /// Runs a command and safely decodes the result\n  Future<String> runSafe(\n    String command, {\n    SystemType? systemType,\n    String? context,\n  }) async {\n    // Let SSH errors propagate with their original type (e.g., SSHError subclasses)\n    final result = await run(command);\n    \n    // Only catch decoding failures and add context\n    try {\n      return SSHDecoder.decode(\n        result,\n        isWindows: systemType == SystemType.windows,\n        context: context,\n      );\n    } on FormatException catch (e) {\n      throw Exception(\n        'Failed to decode command output${context != null ? \" [$context]\" : \"\"}: $e',\n      );\n    }\n  }\n\n  /// Executes a command with stdin and safely decodes stdout/stderr\n  Future<(String stdout, String stderr)> execSafe(\n    void Function(SSHSession session) callback, {\n    required String entry,\n    SystemType? systemType,\n    String? context,\n  }) async {\n    final stdoutBuilder = BytesBuilder(copy: false);\n    final stderrBuilder = BytesBuilder(copy: false);\n    final stdoutDone = Completer<void>();\n    final stderrDone = Completer<void>();\n\n    final session = await execute(entry);\n\n    session.stdout.listen(\n      (e) {\n        stdoutBuilder.add(e);\n      },\n      onDone: stdoutDone.complete,\n      onError: stdoutDone.completeError,\n    );\n\n    session.stderr.listen(\n      (e) {\n        stderrBuilder.add(e);\n      },\n      onDone: stderrDone.complete,\n      onError: stderrDone.completeError,\n    );\n\n    callback(session);\n\n    await stdoutDone.future;\n    await stderrDone.future;\n\n    final stdoutBytes = stdoutBuilder.takeBytes();\n    final stderrBytes = stderrBuilder.takeBytes();\n\n    // Only catch decoding failures, let other errors propagate\n    String stdout;\n    try {\n      stdout = SSHDecoder.decode(\n        stdoutBytes,\n        isWindows: systemType == SystemType.windows,\n        context: context != null ? '$context (stdout)' : 'stdout',\n      );\n    } on FormatException catch (e) {\n      throw Exception(\n        'Failed to decode stdout${context != null ? \" [$context]\" : \"\"}: $e',\n      );\n    }\n\n    String stderr;\n    try {\n      stderr = SSHDecoder.decode(\n        stderrBytes,\n        isWindows: systemType == SystemType.windows,\n        context: context != null ? '$context (stderr)' : 'stderr',\n      );\n    } on FormatException catch (e) {\n      throw Exception(\n        'Failed to decode stderr${context != null ? \" [$context]\" : \"\"}: $e',\n      );\n    }\n\n    return (stdout, stderr);\n  }\n}\n"
  },
  {
    "path": "lib/core/route.dart",
    "content": "import 'package:server_box/data/model/server/server_private_info.dart';\n\n/// The args class for [AppRoute].\nfinal class SpiRequiredArgs {\n  /// The only required argument for this class.\n  final Spi spi;\n\n  const SpiRequiredArgs(this.spi);\n}\n"
  },
  {
    "path": "lib/core/service/ssh_discovery.dart",
    "content": "import 'dart:async';\nimport 'dart:collection';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/server/discovery_result.dart';\n\nclass SshDiscoveryService {\n  static const _sshPort = 22;\n\n  static Future<SshDiscoveryReport> discover([SshDiscoveryConfig config = const SshDiscoveryConfig()]) async {\n    final t0 = DateTime.now();\n    final candidates = <InternetAddress>{};\n\n    // 1) Get neighbors from ARP/NDP tables\n    candidates.addAll(await _neighborsIPv4());\n    candidates.addAll(await _neighborsIPv6());\n\n    // 2) Enumerate small subnets from local interfaces (IPv4 only)\n    final cidrs = await _localIPv4Cidrs();\n    for (final c in cidrs) {\n      if (c.prefix >= 24 && c.prefix <= 30) {\n        candidates.addAll(c.enumerateHosts(limit: config.hostEnumerationLimit));\n      }\n    }\n\n    // 3) Optional: mDNS/Bonjour SSH services\n    if (config.enableMdns) {\n      candidates.addAll(await _mdnsSshCandidates());\n    }\n\n    // Filter out unwanted addresses: loopback, link-local, 0.0.0.0, broadcast, multicast\n    candidates.removeWhere(\n      (a) => a.isLoopback || a.isLinkLocal || a.address == '0.0.0.0' || _isBroadcastOrMulticast(a),\n    );\n\n    // 4) Concurrent SSH port scanning\n    final scanner = _Scanner(\n      timeout: Duration(milliseconds: config.timeoutMs),\n      maxConcurrency: config.maxConcurrency,\n    );\n\n    final results = await scanner.scan(candidates.toList(growable: false));\n    results.sort((a, b) => a.addr.address.compareTo(b.addr.address));\n\n    final discoveryResults = results\n        .map((r) => SshDiscoveryResult(ip: r.addr.address, port: _sshPort, banner: r.banner?.trim()))\n        .toList();\n\n    return SshDiscoveryReport(\n      generatedAt: DateTime.now().toIso8601String(),\n      durationMs: DateTime.now().difference(t0).inMilliseconds,\n      count: discoveryResults.length,\n      items: discoveryResults,\n    );\n  }\n\n  static Future<String?> _run(String exe, List<String> args, {Duration? timeout}) async {\n    try {\n      final p = await Process.start(exe, args, runInShell: false);\n      final out = await p.stdout\n          .transform(utf8.decoder)\n          .join()\n          .timeout(\n            timeout ?? const Duration(seconds: 5),\n            onTimeout: () {\n              p.kill();\n              return '';\n            },\n          );\n      final code = await p.exitCode;\n      if (code == 0) return out;\n      // Some tools return non-zero but still have useful output\n      if (out.trim().isNotEmpty) return out;\n      return null;\n    } catch (e, s) {\n      Loggers.app.warning('Failed to run command: $exe ${args.join(' ')}', e, s);\n      return null;\n    }\n  }\n\n  static bool get _isLinux => Platform.isLinux;\n  static bool get _isMac => Platform.isMacOS;\n\n  static Future<Set<InternetAddress>> _neighborsIPv4() async {\n    final set = <InternetAddress>{};\n    if (_isLinux) {\n      final s = await _run('ip', ['neigh']);\n      if (s != null) {\n        for (final line in const LineSplitter().convert(s)) {\n          final tok = line.split(RegExp(r'\\s+'));\n          if (tok.isNotEmpty) {\n            final ip = tok[0];\n            if (InternetAddress.tryParse(ip)?.type == InternetAddressType.IPv4) {\n              set.add(InternetAddress(ip));\n            }\n          }\n        }\n      }\n    } else if (_isMac) {\n      final s = await _run('/usr/sbin/arp', ['-an']);\n      if (s != null) {\n        int matchCount = 0;\n        for (final line in const LineSplitter().convert(s)) {\n          final m = RegExp(r'\\((\\d+\\.\\d+\\.\\d+\\.\\d+)\\)').firstMatch(line);\n          if (m != null) {\n            set.add(InternetAddress(m.group(1)!));\n            matchCount++;\n          }\n        }\n        if (matchCount == 0) {\n          Loggers.app.warning(\n            '[ssh_discovery] Warning: No ARP entries parsed on macOS. Output may be unexpected or localized. Output sample: ${s.length > 100 ? '${s.substring(0, 100)}...' : s}',\n          );\n        }\n      }\n    }\n    return set;\n  }\n\n  static Future<Set<InternetAddress>> _neighborsIPv6() async {\n    final set = <InternetAddress>{};\n    if (_isLinux) {\n      final s = await _run('ip', ['-6', 'neigh']);\n      if (s != null) {\n        for (final line in const LineSplitter().convert(s)) {\n          final ip = line.split(RegExp(r'\\s+')).firstOrNull;\n          if (ip != null && InternetAddress.tryParse(ip)?.type == InternetAddressType.IPv6) {\n            set.add(InternetAddress(ip));\n          }\n        }\n      }\n    } else if (_isMac) {\n      final s = await _run('/usr/sbin/ndp', ['-a']);\n      if (s != null) {\n        for (final line in const LineSplitter().convert(s)) {\n          final ip = line.trim().split(RegExp(r'\\s+')).firstOrNull;\n          if (ip != null && InternetAddress.tryParse(ip)?.type == InternetAddressType.IPv6) {\n            set.add(InternetAddress(ip));\n          }\n        }\n      }\n    }\n    return set;\n  }\n\n  static Future<List<_Cidr>> _localIPv4Cidrs() async {\n    final res = <_Cidr>[];\n    if (_isLinux) {\n      final s = await _run('ip', ['-o', '-4', 'addr', 'show', 'scope', 'global']);\n      if (s != null) {\n        for (final line in const LineSplitter().convert(s)) {\n          final m = RegExp(r'inet\\s+(\\d+\\.\\d+\\.\\d+\\.\\d+)\\/(\\d+)').firstMatch(line);\n          if (m != null) {\n            final ip = InternetAddress(m.group(1)!);\n            final prefix = int.parse(m.group(2)!);\n            final mask = _prefixToMask(prefix);\n            final net = _networkAddress(ip, mask);\n            final brd = _broadcastAddress(ip, mask);\n            res.add(_Cidr(ip, prefix, mask, net, brd));\n          }\n        }\n      }\n    } else if (_isMac) {\n      final s = await _run('/sbin/ifconfig', []);\n      if (s != null) {\n        for (final raw in const LineSplitter().convert(s)) {\n          final line = raw.trimRight();\n          final ifMatch = RegExp(r'^([a-z0-9]+):').firstMatch(line);\n          if (ifMatch != null) {\n            continue;\n          }\n          if (line.contains('inet ') && !line.contains('127.0.0.1')) {\n            try {\n              final ipm = RegExp(\n                r'inet\\s+(\\d+\\.\\d+\\.\\d+\\.\\d+)\\s+netmask\\s+0x([0-9a-fA-F]+)(?:\\s+broadcast\\s+(\\d+\\.\\d+\\.\\d+\\.\\d+))?',\n              ).firstMatch(line);\n              if (ipm == null) {\n                Loggers.app.warning('[ssh_discovery] Warning: Unexpected ifconfig line format: $line');\n                continue;\n              }\n              final ip = InternetAddress(ipm.group(1)!);\n              final hexMask = int.parse(ipm.group(2)!, radix: 16);\n              final dotted =\n                  '${(hexMask >> 24) & 0xff}.${(hexMask >> 16) & 0xff}.${(hexMask >> 8) & 0xff}.${hexMask & 0xff}';\n              final mask = InternetAddress(dotted);\n              final prefix = _maskToPrefix(mask.address);\n              final net = _networkAddress(ip, mask);\n              final brd = InternetAddress(ipm.group(3) ?? _broadcastAddress(ip, mask).address);\n              res.add(_Cidr(ip, prefix, mask, net, brd));\n            } catch (e) {\n              Loggers.app.warning('[ssh_discovery] Error parsing ifconfig output: $e, line: $line');\n              continue;\n            }\n          }\n        }\n      }\n    }\n    return res;\n  }\n\n  static bool _isBroadcastOrMulticast(InternetAddress a) {\n    // IPv4 broadcast: ends with .255 or is 255.255.255.255\n    if (a.type == InternetAddressType.IPv4) {\n      if (a.address == '255.255.255.255') return true;\n      if (a.address.split('.').last == '255') return true;\n      // Multicast: 224.0.0.0 - 239.255.255.255\n      final firstOctet = int.tryParse(a.address.split('.').first) ?? 0;\n      if (firstOctet >= 224 && firstOctet <= 239) return true;\n    } else if (a.type == InternetAddressType.IPv6) {\n      // IPv6 multicast: starts with ff\n      if (a.address.toLowerCase().startsWith('ff')) return true;\n    }\n    return false;\n  }\n\n  static Future<Set<InternetAddress>> _mdnsSshCandidates() async {\n    final set = <InternetAddress>{};\n    if (_isMac) {\n      try {\n        final proc = await Process.start('/usr/bin/dns-sd', ['-B', '_ssh._tcp']);\n        final lines = <String>[];\n        final subscription = proc.stdout\n            .transform(utf8.decoder)\n            .transform(const LineSplitter())\n            .listen(lines.add);\n        await Future<void>.delayed(const Duration(seconds: 2));\n        proc.kill();\n        await subscription.cancel();\n\n        for (final l in lines) {\n          final m = RegExp(r'Add\\s+\\d+\\s+(\\S+)\\.\\s+_ssh\\._tcp\\.').firstMatch(l);\n          if (m != null) {\n            final name = m.group(1)!;\n            final det = await _run('/usr/bin/dns-sd', [\n              '-L',\n              name,\n              '_ssh._tcp',\n              'local.',\n            ], timeout: const Duration(seconds: 3));\n            if (det != null) {\n              for (final ip in RegExp(\n                r'Address\\s*=\\s*([0-9a-fA-F:\\.]+)',\n              ).allMatches(det).map((e) => e.group(1)!)) {\n                final parsed = InternetAddress.tryParse(ip);\n                if (parsed != null) set.add(parsed);\n              }\n            }\n          }\n        }\n      } catch (e, s) {\n        Loggers.app.warning('Failed to discover mDNS SSH candidates on macOS', e, s);\n      }\n    } else if (_isLinux) {\n      final s = await _run('/usr/bin/avahi-browse', ['-rat', '_ssh._tcp']);\n      if (s != null) {\n        for (final ip in RegExp(\n          r'address = \\[(.*?)\\]',\n        ).allMatches(s).map((m) => m.group(1)!).where((e) => e.isNotEmpty)) {\n          final parsed = InternetAddress.tryParse(ip);\n          if (parsed != null) set.add(parsed);\n        }\n      }\n    }\n    return set;\n  }\n}\n\nclass _Cidr {\n  final InternetAddress ip;\n  final int prefix;\n  final InternetAddress netmask;\n  final InternetAddress network;\n  final InternetAddress broadcast;\n\n  _Cidr(this.ip, this.prefix, this.netmask, this.network, this.broadcast);\n\n  Iterable<InternetAddress> enumerateHosts({int? limit}) sync* {\n    final n = _ipv4ToInt(network.address);\n    final b = _ipv4ToInt(broadcast.address);\n    int emitted = 0;\n    for (int v = n + 1; v <= b - 1; v++) {\n      if (limit != null && emitted >= limit) break;\n      emitted++;\n      yield InternetAddress(_intToIPv4(v));\n    }\n  }\n\n  @override\n  String toString() => '${network.address}/$prefix';\n}\n\nclass _ScanResult {\n  final InternetAddress addr;\n  final String? banner;\n  _ScanResult(this.addr, this.banner);\n}\n\nclass _Scanner {\n  final Duration timeout;\n  final int maxConcurrency;\n  _Scanner({required this.timeout, required this.maxConcurrency});\n\n  Future<List<_ScanResult>> scan(List<InternetAddress> addrs) async {\n    final sem = _Semaphore(maxConcurrency);\n    final futures = <Future<_ScanResult?>>[];\n    for (final a in addrs) {\n      futures.add(_guarded(sem, () => _probeSsh(a)));\n    }\n    final out = await Future.wait(futures);\n    return out.whereType<_ScanResult>().toList();\n  }\n\n  Future<_ScanResult?> _probeSsh(InternetAddress ip) async {\n    Socket? socket;\n    StreamSubscription? sub;\n    try {\n      socket = await Socket.connect(ip, SshDiscoveryService._sshPort, timeout: timeout);\n      socket.timeout(timeout);\n      final c = Completer<String?>();\n      sub = socket.listen(\n        (data) {\n          final s = utf8.decode(data, allowMalformed: true);\n          final line = s.split('\\n').firstWhere((_) => true, orElse: () => s);\n          if (!c.isCompleted) {\n            c.complete(line.trim());\n            sub?.cancel();\n          }\n        },\n        onDone: () {\n          if (!c.isCompleted) c.complete(null);\n        },\n        onError: (_) {\n          if (!c.isCompleted) c.complete(null);\n        },\n      );\n      final banner = await c.future.timeout(timeout, onTimeout: () => null);\n      return _ScanResult(ip, banner);\n    } catch (e, s) {\n      Loggers.app.warning('Failed to probe SSH at ${ip.address}', e, s);\n      return null;\n    } finally {\n      sub?.cancel();\n      socket?.destroy();\n    }\n  }\n}\n\nclass _Semaphore {\n  int _permits;\n  final Queue<Completer<void>> _q = Queue();\n  _Semaphore(this._permits);\n\n  Future<T> withPermit<T>(Future<T> Function() fn) async {\n    if (_permits > 0) {\n      _permits--;\n      try {\n        return await fn();\n      } finally {\n        _permits++;\n        if (_q.isNotEmpty) _q.removeFirst().complete();\n      }\n    } else {\n      final c = Completer<void>();\n      _q.add(c);\n      await c.future;\n      return withPermit(fn);\n    }\n  }\n}\n\nFuture<T> _guarded<T>(_Semaphore sem, Future<T> Function() fn) => sem.withPermit(fn);\n\n// IPv4 utilities\n\nint _ipv4ToInt(String ip) {\n  final p = ip.split('.').map(int.parse).toList();\n  return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];\n}\n\nString _intToIPv4(int v) => '${(v >> 24) & 0xff}.${(v >> 16) & 0xff}.${(v >> 8) & 0xff}.${v & 0xff}';\n\nInternetAddress _prefixToMask(int prefix) {\n  final mask = prefix == 0 ? 0 : 0xffffffff << (32 - prefix);\n  return InternetAddress(_intToIPv4(mask & 0xffffffff));\n}\n\nint _maskToPrefix(String mask) {\n  final v = _ipv4ToInt(mask);\n  int c = 0;\n  for (int i = 31; i >= 0; i--) {\n    if ((v & (1 << i)) != 0) {\n      c++;\n    } else {\n      break;\n    }\n  }\n  return c;\n}\n\nInternetAddress _networkAddress(InternetAddress ip, InternetAddress mask) {\n  final v = _ipv4ToInt(ip.address) & _ipv4ToInt(mask.address);\n  return InternetAddress(_intToIPv4(v));\n}\n\nInternetAddress _broadcastAddress(InternetAddress ip, InternetAddress mask) {\n  final n = _ipv4ToInt(ip.address) & _ipv4ToInt(mask.address);\n  final b = n | (~_ipv4ToInt(mask.address) & 0xffffffff);\n  return InternetAddress(_intToIPv4(b));\n}\n"
  },
  {
    "path": "lib/core/sync.dart",
    "content": "import 'dart:io';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/app/bak/backup2.dart';\nimport 'package:server_box/data/model/app/bak/utils.dart';\n\nconst bakSync = BakSyncer._();\n\nfinal icloud = ICloud(containerId: 'iCloud.tech.lolli.serverbox');\n\nfinal class BakSyncer extends SyncIface {\n  const BakSyncer._() : super();\n\n  @override\n  Future<void> saveToFile() async {\n    final pwd = await SecureStoreProps.bakPwd.read();\n    await BackupV2.backup(null, pwd?.isEmpty == true ? null : pwd);\n  }\n\n  @override\n  Future<Mergeable> fromFile(String path) async {\n    final content = await File(path).readAsString();\n    final pwd = await SecureStoreProps.bakPwd.read();\n    try {\n      if (Cryptor.isEncrypted(content)) {\n        return MergeableUtils.fromJsonString(content, pwd).$1;\n      }\n      return MergeableUtils.fromJsonString(content).$1;\n    } catch (e, s) {\n      Loggers.app.warning('Failed to parse backup file with password, trying without password', e, s);\n      // Fallback: try without password if detection failed\n      return MergeableUtils.fromJsonString(content).$1;\n    }\n  }\n\n  @override\n  RemoteStorage? get remoteStorage {\n    final icloudEnabled = PrefProps.icloudSync.get();\n    if (icloudEnabled) return icloud;\n\n    final webdavEnabled = PrefProps.webdavSync.get();\n    if (webdavEnabled) return Webdav.shared;\n\n    final gistEnabled = PrefProps.gistSync.get();\n    if (gistEnabled) return GistRs.shared;\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "lib/core/utils/comparator.dart",
    "content": "class ChainComparator<T> {\n  final ChainComparator<T>? _parent;\n  final Comparator<T> _comparator;\n\n  ChainComparator._create(this._parent, this._comparator);\n  ChainComparator.empty() : this._create(null, (a, b) => 0);\n  ChainComparator.create() : this._create(null, (a, b) => 0);\n\n  static ChainComparator<T> comparing<T, F extends Comparable<F>>(F Function(T) extractor) {\n    return ChainComparator._create(null, (a, b) => extractor(a).compareTo(extractor(b)));\n  }\n\n  int compare(T a, T b) {\n    final parent = _parent;\n    if (parent != null) {\n      final int result = parent.compare(a, b);\n      if (result != 0) return result;\n    }\n    return _comparator(a, b);\n  }\n\n  int call(T a, T b) {\n    return compare(a, b);\n  }\n\n  ChainComparator<T> thenCompareBy<F extends Comparable<F>>(\n    F Function(T) extractor, {\n    bool reversed = false,\n  }) {\n    return ChainComparator._create(\n      this,\n      reversed\n          ? (a, b) => extractor(b).compareTo(extractor(a))\n          : (a, b) => extractor(a).compareTo(extractor(b)),\n    );\n  }\n\n  ChainComparator<T> thenWithComparator(Comparator<T> comparator, {bool reversed = false}) {\n    return ChainComparator._create(this, !reversed ? comparator : (a, b) => comparator(b, a));\n  }\n\n  ChainComparator<T> thenCompareByReversed<F extends Comparable<F>>(F Function(T) extractor) {\n    return ChainComparator._create(this, (a, b) => -extractor(a).compareTo(extractor(b)));\n  }\n\n  ChainComparator<T> thenTrueFirst(bool Function(T) f) {\n    return ChainComparator._create(this, (a, b) {\n      final fa = f(a), fb = f(b);\n      return fa == fb ? 0 : (fa ? -1 : 1);\n    });\n  }\n\n  ChainComparator<T> reversed() {\n    return ChainComparator._create(null, (a, b) => compare(b, a));\n  }\n}\n\nclass Comparators {\n  static Comparator<String> compareStringCaseInsensitive({bool uppercaseFirst = false}) {\n    return (String a, String b) {\n      final r = a.toLowerCase().compareTo(b.toLowerCase());\n      if (r != 0) return r;\n      return uppercaseFirst ? a.compareTo(b) : b.compareTo(a);\n    };\n  }\n}\n\n/*\n\nComparator.comparing<Type1, Type2>(Type1::getType2)\n.thenCompare<Type3>(Type1::getType3)\n.thenCompare<Type4>(Type1::getType4)\n.thenCompareReversed<Type5>(Type1::getType5)\n\n */\n"
  },
  {
    "path": "lib/core/utils/host_key_helper.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/core/utils/server.dart';\nimport 'package:server_box/core/utils/ssh_auth.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/res/store.dart';\n\nFuture<bool> ensureHostKeyAcceptedForSftp(BuildContext context, Spi spi) async {\n  final known = Stores.setting.sshKnownHostFingerprints.get();\n  final hostId = spi.id.isNotEmpty ? spi.id : spi.oldId;\n  final prefix = '$hostId::';\n  if (known.keys.any((key) => key.startsWith(prefix))) {\n    return true;\n  }\n\n  final (result, error) = await context.showLoadingDialog<bool>(\n    fn: () async {\n      await ensureKnownHostKey(\n        spi,\n        onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi, ctx: context),\n      );\n      return true;\n    },\n  );\n  return error == null && result == true;\n}\n"
  },
  {
    "path": "lib/core/utils/jump_chain.dart",
    "content": "import 'package:server_box/data/model/server/server_private_info.dart';\n\n/// Returns `true` when assigning [candidateJumpId] to [currentServerId]\n/// would create a jump-server cycle.\nbool wouldCreateJumpCycle({\n  required String? currentServerId,\n  required String? candidateJumpId,\n  required Map<String, Spi> serversById,\n}) {\n  if (candidateJumpId == null || candidateJumpId.isEmpty) {\n    return false;\n  }\n\n  final visited = <String>{};\n  var checkingId = candidateJumpId;\n\n  while (true) {\n    if (currentServerId != null && checkingId == currentServerId) {\n      return true;\n    }\n    if (!visited.add(checkingId)) {\n      // Existing malformed cycle is treated as invalid to prevent linking into it.\n      return true;\n    }\n\n    final nextId = serversById[checkingId]?.jumpId;\n    if (nextId == null || nextId.isEmpty) {\n      return false;\n    }\n    checkingId = nextId;\n  }\n}\n\n/// Collects all reachable jump servers from [spi.jumpId], keyed by server id.\nMap<String, Spi> collectJumpServers({\n  required Spi spi,\n  required Map<String, Spi> serversById,\n}) {\n  final chain = <String, Spi>{};\n  final visited = <String>{};\n  var jumpId = spi.jumpId;\n\n  while (jumpId != null && jumpId.isNotEmpty && visited.add(jumpId)) {\n    final jumpSpi = serversById[jumpId];\n    if (jumpSpi == null) {\n      break;\n    }\n    chain[jumpSpi.id] = jumpSpi;\n    jumpId = jumpSpi.jumpId;\n  }\n\n  return chain;\n}\n"
  },
  {
    "path": "lib/core/utils/misc.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:plain_notification_token/plain_notification_token.dart';\n\nFuture<String?> getToken() async {\n  if (!isIOS) return null;\n  final instance = ApnsToken()..requestPermission();\n  // Wait until Permission dialog closed\n  await instance.onIosSettingsRegistered.first;\n  return await instance.getToken();\n}\n"
  },
  {
    "path": "lib/core/utils/server.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:dartssh2/dartssh2.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/core/app_navigator.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/app/error.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/res/store.dart';\n\n/// Must put this func out of any Class.\n///\n/// Because of this function is called by [compute].\n///\n/// https://stackoverflow.com/questions/51998995/invalid-arguments-illegal-argument-in-isolate-message-object-is-a-closure\nList<SSHKeyPair> loadIndentity(String key) {\n  return SSHKeyPair.fromPem(key);\n}\n\n/// [args] : [key, pwd]\nString decyptPem(List<String> args) {\n  /// skip when the key is not encrypted, or will throw exception\n  if (!SSHKeyPair.isEncryptedPem(args[0])) return args[0];\n  final sshKey = SSHKeyPair.fromPem(args[0], args[1]);\n  return sshKey.first.toPem();\n}\n\nenum GenSSHClientStatus { socket, key, pwd }\n\nString getPrivateKey(String id) {\n  final pki = Stores.key.fetchOne(id);\n  if (pki == null) {\n    throw SSHErr(\n      type: SSHErrType.noPrivateKey,\n      message: l10n.privateKeyNotFoundFmt(id),\n    );\n  }\n  return pki.key;\n}\n\nFuture<SSHClient> genClient(\n  Spi spi, {\n  void Function(GenSSHClientStatus)? onStatus,\n\n  /// Only pass this param if using multi-threading and key login\n  String? privateKey,\n\n  /// Only pass this param if using multi-threading and key login\n  String? jumpPrivateKey,\n\n  /// Prefer this map in isolate mode, fallback to [Stores.key] otherwise.\n  Map<String, String>? privateKeysByKeyId,\n\n  /// Prefer this map in isolate mode, fallback to [Stores.server] otherwise.\n  Map<String, Spi>? jumpSpisById,\n  Duration timeout = const Duration(seconds: 5),\n\n  /// [Spi] of the jump server\n  ///\n  /// Must pass this param if using multi-threading and key login\n  Spi? jumpSpi,\n\n  /// Handle keyboard-interactive authentication\n  SSHUserInfoRequestHandler? onKeyboardInteractive,\n  Map<String, String>? knownHostFingerprints,\n  void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,\n  Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,\n  Set<String>? visitedServerIds,\n}) async {\n  final chainVisitedServerIds = visitedServerIds ?? <String>{};\n  final currentServerId = _hostIdentifier(spi);\n  if (!chainVisitedServerIds.add(currentServerId)) {\n    throw SSHErr(\n      type: SSHErrType.connect,\n      message:\n          'Invalid jump chain: cycle detected at ${spi.name} ($currentServerId)',\n    );\n  }\n\n  onStatus?.call(GenSSHClientStatus.socket);\n\n  final hostKeyCache = Map<String, String>.from(\n    knownHostFingerprints ?? _loadKnownHostFingerprints(),\n  );\n  final hostKeyPersist = onHostKeyAccepted ?? _persistHostKeyFingerprint;\n  final hostKeyPrompt = onHostKeyPrompt ?? _defaultHostKeyPrompt;\n\n  String? alterUser;\n\n  final socket = await () async {\n    // Proxy\n    final jumpSpi_ = () {\n      // Multi-thread or key login\n      if (jumpSpi != null) return jumpSpi;\n      // Main thread\n      final jumpId = spi.jumpId;\n      if (jumpId != null) {\n        return jumpSpisById?[jumpId] ?? Stores.server.box.get(jumpId);\n      }\n    }();\n    if (jumpSpi_ != null) {\n      String? nextJumpPrivateKey;\n      final jumpSpiKeyId = jumpSpi_.keyId;\n      if (jumpSpi != null &&\n          jumpSpi.id == jumpSpi_.id &&\n          jumpPrivateKey != null) {\n        // Isolate mode may preload first-hop key and pass it via [jumpPrivateKey].\n        nextJumpPrivateKey = jumpPrivateKey;\n      } else if (jumpSpiKeyId != null) {\n        nextJumpPrivateKey = privateKeysByKeyId?[jumpSpiKeyId];\n      }\n\n      final jumpClient = await genClient(\n        jumpSpi_,\n        privateKey: nextJumpPrivateKey,\n        privateKeysByKeyId: privateKeysByKeyId,\n        jumpSpisById: jumpSpisById,\n        timeout: timeout,\n        knownHostFingerprints: hostKeyCache,\n        onHostKeyAccepted: hostKeyPersist,\n        onHostKeyPrompt: hostKeyPrompt,\n        visitedServerIds: chainVisitedServerIds,\n      );\n\n      return await jumpClient.forwardLocal(spi.ip, spi.port);\n    }\n\n    // Direct\n    try {\n      return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);\n    } catch (e) {\n      Loggers.app.warning('genClient', e);\n      if (spi.alterUrl == null) rethrow;\n      try {\n        final res = spi.parseAlterUrl();\n        alterUser = res.$2;\n        return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);\n      } catch (e) {\n        Loggers.app.warning('genClient alterUrl', e);\n        rethrow;\n      }\n    }\n  }();\n\n  final hostKeyVerifier = _HostKeyVerifier(\n    spi: spi,\n    cache: hostKeyCache,\n    persistCallback: hostKeyPersist,\n    prompt: hostKeyPrompt,\n  );\n\n  final keyId = spi.keyId;\n  if (keyId == null) {\n    onStatus?.call(GenSSHClientStatus.pwd);\n    return SSHClient(\n      socket,\n      username: alterUser ?? spi.user,\n      onPasswordRequest: () => spi.pwd,\n      onUserInfoRequest: onKeyboardInteractive,\n      onVerifyHostKey: hostKeyVerifier.call,\n      // printDebug: debugPrint,\n      // printTrace: debugPrint,\n    );\n  }\n  privateKey ??= privateKeysByKeyId?[keyId] ?? getPrivateKey(keyId);\n\n  onStatus?.call(GenSSHClientStatus.key);\n  return SSHClient(\n    socket,\n    username: spi.user,\n    // Must use [compute] here, instead of [Computer.shared.start]\n    identities: await compute(loadIndentity, privateKey),\n    onUserInfoRequest: onKeyboardInteractive,\n    onVerifyHostKey: hostKeyVerifier.call,\n    // printDebug: debugPrint,\n    // printTrace: debugPrint,\n  );\n}\n\ntypedef _HostKeyPersistCallback =\n    void Function(String storageKey, String fingerprintHex);\n\nclass HostKeyPromptInfo {\n  HostKeyPromptInfo({\n    required this.spi,\n    required this.keyType,\n    required this.fingerprintHex,\n    required this.fingerprintBase64,\n    required this.isMismatch,\n    this.previousFingerprintHex,\n  });\n\n  final Spi spi;\n  final String keyType;\n  final String fingerprintHex;\n  final String fingerprintBase64;\n  final bool isMismatch;\n  final String? previousFingerprintHex;\n}\n\nclass _HostKeyVerifier {\n  _HostKeyVerifier({\n    required this.spi,\n    required Map<String, String> cache,\n    required this.prompt,\n    this.persistCallback,\n  }) : _cache = cache;\n\n  final Spi spi;\n  final Map<String, String> _cache;\n  final _HostKeyPersistCallback? persistCallback;\n  final Future<bool> Function(HostKeyPromptInfo info) prompt;\n\n  Future<bool> call(String keyType, Uint8List fingerprintBytes) async {\n    final storageKey = _hostKeyStorageKey(spi, keyType);\n    final fingerprintHex = _fingerprintToHex(fingerprintBytes);\n    final fingerprintBase64 = _fingerprintToBase64(fingerprintBytes);\n    final existing = _cache[storageKey];\n\n    if (existing == null) {\n      final accepted = await prompt(\n        HostKeyPromptInfo(\n          spi: spi,\n          keyType: keyType,\n          fingerprintHex: fingerprintHex,\n          fingerprintBase64: fingerprintBase64,\n          isMismatch: false,\n        ),\n      );\n      if (!accepted) {\n        Loggers.app.warning(\n          'User rejected new SSH host key for ${spi.name} ($keyType).',\n        );\n        return false;\n      }\n      _cache[storageKey] = fingerprintHex;\n      persistCallback?.call(storageKey, fingerprintHex);\n      Loggers.app.info('Trusted SSH host key for ${spi.name} ($keyType).');\n      return true;\n    }\n\n    if (existing == fingerprintHex) {\n      return true;\n    }\n\n    final accepted = await prompt(\n      HostKeyPromptInfo(\n        spi: spi,\n        keyType: keyType,\n        fingerprintHex: fingerprintHex,\n        fingerprintBase64: fingerprintBase64,\n        isMismatch: true,\n        previousFingerprintHex: existing,\n      ),\n    );\n    if (!accepted) {\n      Loggers.app.warning(\n        'SSH host key mismatch for ${spi.name}',\n        'expected $existing but received $fingerprintHex ($keyType)',\n      );\n      return false;\n    }\n\n    _cache[storageKey] = fingerprintHex;\n    persistCallback?.call(storageKey, fingerprintHex);\n    Loggers.app.warning(\n      'Updated stored SSH host key for ${spi.name} ($keyType) after user confirmation.',\n    );\n    return true;\n  }\n}\n\nMap<String, String> _loadKnownHostFingerprints() {\n  try {\n    final prop = Stores.setting.sshKnownHostFingerprints;\n    return Map<String, String>.from(prop.get());\n  } catch (e, stack) {\n    Loggers.app.warning('Load SSH host key fingerprints failed', e, stack);\n    return <String, String>{};\n  }\n}\n\nvoid _persistHostKeyFingerprint(String storageKey, String fingerprintHex) {\n  try {\n    final prop = Stores.setting.sshKnownHostFingerprints;\n    final updated = Map<String, String>.from(prop.get());\n    if (updated[storageKey] == fingerprintHex) {\n      return;\n    }\n    updated[storageKey] = fingerprintHex;\n    prop.put(updated);\n    Loggers.app.info('Stored SSH host key fingerprint for $storageKey');\n  } catch (e, stack) {\n    Loggers.app.warning('Persist SSH host key fingerprint failed', e, stack);\n  }\n}\n\nFuture<bool> _defaultHostKeyPrompt(HostKeyPromptInfo info) async {\n  final ctx = AppNavigator.context;\n  if (ctx == null) {\n    Loggers.app.warning(\n      'Host key prompt skipped: navigator context unavailable.',\n    );\n    return false;\n  }\n\n  final hostLine = '${info.spi.user}@${info.spi.ip}:${info.spi.port}';\n  final description = info.isMismatch\n      ? l10n.sshHostKeyChangedDesc(info.spi.name)\n      : l10n.sshHostKeyNewDesc(info.spi.name);\n\n  final result = await ctx.showRoundDialog<bool>(\n    title: libL10n.attention,\n    barrierDismiss: false,\n    child: Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text(description),\n        const SizedBox(height: 12),\n        SelectableText('${libL10n.server}: ${info.spi.name}'),\n        SelectableText('${libL10n.addr}: $hostLine'),\n        SelectableText('${l10n.sshHostKeyType}: ${info.keyType}'),\n        SelectableText(l10n.sshHostKeyFingerprintMd5Hex(info.fingerprintHex)),\n        SelectableText(\n          l10n.sshHostKeyFingerprintMd5Base64(info.fingerprintBase64),\n        ),\n        if (info.previousFingerprintHex != null) ...[\n          const SizedBox(height: 12),\n          SelectableText(\n            l10n.sshHostKeyStoredFingerprint(info.previousFingerprintHex!),\n          ),\n        ],\n      ],\n    ),\n    actions: [\n      TextButton(onPressed: () => ctx.pop(false), child: Text(libL10n.cancel)),\n      TextButton(onPressed: () => ctx.pop(true), child: Text(libL10n.ok)),\n    ],\n  );\n\n  return result ?? false;\n}\n\nFuture<void> ensureKnownHostKey(\n  Spi spi, {\n  Duration timeout = const Duration(seconds: 5),\n  SSHUserInfoRequestHandler? onKeyboardInteractive,\n  Map<String, Spi>? jumpSpisById,\n  Set<String>? visitedServerIds,\n}) async {\n  final chainVisitedServerIds = visitedServerIds ?? <String>{};\n  final currentServerId = _hostIdentifier(spi);\n  if (!chainVisitedServerIds.add(currentServerId)) {\n    throw SSHErr(\n      type: SSHErrType.connect,\n      message:\n          'Invalid jump chain: cycle detected at ${spi.name} ($currentServerId)',\n    );\n  }\n\n  final cache = _loadKnownHostFingerprints();\n  if (_hasKnownHostFingerprintForSpi(spi, cache)) {\n    return;\n  }\n\n  final jumpId = spi.jumpId;\n  final jumpSpi = jumpId != null\n      ? (jumpSpisById?[jumpId] ?? Stores.server.box.get(jumpId))\n      : null;\n  if (jumpSpi != null && !_hasKnownHostFingerprintForSpi(jumpSpi, cache)) {\n    await ensureKnownHostKey(\n      jumpSpi,\n      timeout: timeout,\n      onKeyboardInteractive: onKeyboardInteractive,\n      jumpSpisById: jumpSpisById,\n      visitedServerIds: chainVisitedServerIds,\n    );\n    cache.addAll(_loadKnownHostFingerprints());\n    if (_hasKnownHostFingerprintForSpi(spi, cache)) return;\n  }\n\n  final client = await genClient(\n    spi,\n    timeout: timeout,\n    onKeyboardInteractive: onKeyboardInteractive,\n    knownHostFingerprints: cache,\n  );\n\n  try {\n    await client.authenticated;\n  } finally {\n    client.close();\n  }\n}\n\nbool _hasKnownHostFingerprintForSpi(Spi spi, Map<String, String> cache) {\n  final prefix = '${_hostIdentifier(spi)}::';\n  return cache.keys.any((key) => key.startsWith(prefix));\n}\n\nString _hostKeyStorageKey(Spi spi, String keyType) {\n  final base = _hostIdentifier(spi);\n  return '$base::$keyType';\n}\n\nString _hostIdentifier(Spi spi) => spi.id.isNotEmpty ? spi.id : spi.oldId;\n\nString _fingerprintToHex(Uint8List fingerprint) {\n  final buffer = StringBuffer();\n  for (var i = 0; i < fingerprint.length; i++) {\n    if (i > 0) buffer.write(':');\n    buffer.write(fingerprint[i].toRadixString(16).padLeft(2, '0'));\n  }\n  return buffer.toString();\n}\n\nString _fingerprintToBase64(Uint8List fingerprint) =>\n    base64.encode(fingerprint);\n"
  },
  {
    "path": "lib/core/utils/server_dedup.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/store/server.dart';\n\nclass ServerDeduplication {\n  /// Remove duplicate servers from the import list based on existing servers\n  /// Returns the deduplicated list\n  static List<Spi> deduplicateServers(List<Spi> importedServers) {\n    final existingServers = ServerStore.instance.fetch();\n    final deduplicated = <Spi>[];\n    \n    for (final imported in importedServers) {\n      if (!_isDuplicate(imported, existingServers)) {\n        deduplicated.add(imported);\n      }\n    }\n    \n    return deduplicated;\n  }\n  \n  /// Check if an imported server is a duplicate of an existing server\n  static bool _isDuplicate(Spi imported, List<Spi> existing) {\n    for (final existingSpi in existing) {\n      if (imported.isSameAs(existingSpi)) {\n        return true;\n      }\n    }\n    \n    return false;\n  }\n  \n  /// Resolve name conflicts by appending suffixes\n  static List<Spi> resolveNameConflicts(List<Spi> importedServers) {\n    final existingServers = ServerStore.instance.fetch();\n    final existingNames = existingServers.map((s) => s.name).toSet();\n    final processedNames = <String>{};\n    final result = <Spi>[];\n    \n    for (final server in importedServers) {\n      String newName = server.name;\n      int suffix = 1;\n      \n      // Check against both existing servers and already processed servers\n      while (existingNames.contains(newName) || processedNames.contains(newName)) {\n        newName = '${server.name} ($suffix)';\n        suffix++;\n      }\n      \n      processedNames.add(newName);\n      \n      if (newName != server.name) {\n        result.add(server.copyWith(name: newName));\n      } else {\n        result.add(server);\n      }\n    }\n    \n    return result;\n  }\n  \n  /// Get summary of import operation\n  static ImportSummary getImportSummary(List<Spi> originalList, List<Spi> deduplicatedList) {\n    final duplicateCount = originalList.length - deduplicatedList.length;\n    return ImportSummary(\n      total: originalList.length,\n      duplicates: duplicateCount,\n      toImport: deduplicatedList.length,\n    );\n  }\n\n  /// Import servers with deduplication and show appropriate notifications\n  /// Returns the number of servers actually imported\n  /// Note: Caller must check mounted before calling this method\n  /// If resolvedServers is provided, it should be pre-filtered (non-empty)\n  /// [originalCount] should be provided when passing resolvedServers to show\n  /// the true pre-dedup count in messages\n  static Future<int> importServersWithNotification({\n    List<Spi>? servers,\n    required WidgetRef ref,\n    required BuildContext context,\n    List<Spi>? resolvedServers,\n    int? originalCount,\n    required String Function(String) allExistMessage,\n    required String Function(String) importedMessage,\n  }) async {\n    assert(servers != null || resolvedServers != null, \n        'Either servers or resolvedServers must be provided');\n    \n    final count = originalCount ?? servers?.length ?? resolvedServers!.length;\n    final resolved = resolvedServers ?? _resolveServers(servers!);\n\n    if (resolved.isEmpty) {\n      context.showSnackBar(allExistMessage('$count'));\n      return 0;\n    }\n\n    for (final server in resolved) {\n      ref.read(serversProvider.notifier).addServer(server);\n    }\n    context.showSnackBar(importedMessage('${resolved.length}'));\n    return resolved.length;\n  }\n\n  static List<Spi> _resolveServers(List<Spi> servers) {\n    final deduplicated = deduplicateServers(servers);\n    final resolved = resolveNameConflicts(deduplicated);\n    return resolved;\n  }\n}\n\nclass ImportSummary {\n  final int total;\n  final int duplicates;\n  final int toImport;\n  \n  const ImportSummary({\n    required this.total,\n    required this.duplicates,\n    required this.toImport,\n  });\n  \n  bool get hasDuplicates => duplicates > 0;\n  bool get hasItemsToImport => toImport > 0;\n}"
  },
  {
    "path": "lib/core/utils/ssh_auth.dart",
    "content": "import 'dart:async';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\n\nabstract final class KeybordInteractive {\n  static FutureOr<List<String>?> defaultHandle(Spi spi, {BuildContext? ctx}) async {\n    try {\n      final res = await (ctx ?? WidgetsBinding.instance.focusManager.primaryFocus?.context)?.showPwdDialog(\n        title: libL10n.pwd,\n        id: spi.id,\n        label: spi.id,\n      );\n      return res == null ? null : [res];\n    } catch (e) {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/core/utils/ssh_config.dart",
    "content": "import 'dart:io';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\n\n/// Utility class to parse SSH config files under `~/.ssh/config`\nabstract final class SSHConfig {\n  static const String _defaultPath = '~/.ssh/config';\n\n  static String? get _homePath {\n    final homePath = isWindows ? Platform.environment['USERPROFILE'] : Platform.environment['HOME'];\n    if (homePath == null || homePath.isEmpty) {\n      return null;\n    }\n    return homePath;\n  }\n\n  /// Get possible SSH config file paths, with macOS-specific handling\n  static List<String> get _possibleConfigPaths {\n    final paths = <String>[];\n    final homePath = _homePath;\n\n    if (homePath != null) {\n      // Standard path\n      paths.add('$homePath/.ssh/config');\n\n      // On macOS, also try the actual user home directory\n      if (isMacOS) {\n        // Try to get the real user home directory\n        final username = Platform.environment['USER'];\n        if (username != null) {\n          paths.add('/Users/$username/.ssh/config');\n        }\n      }\n    }\n\n    return paths;\n  }\n\n  /// Parse SSH config file and return a list of Spi objects\n  static Future<List<Spi>> parseConfig([String? configPath]) async {\n    final (file, exists) = configExists(configPath);\n    if (!exists || file == null) {\n      Loggers.app.info('SSH config file does not exist at path: ${configPath ?? _defaultPath}');\n      return [];\n    }\n\n    final content = await file.readAsString();\n    return _parseSSHConfig(content);\n  }\n\n  /// Parse SSH config content\n  static List<Spi> _parseSSHConfig(String content) {\n    final servers = <Spi>[];\n    final lines = content.split('\\n');\n\n    String? currentHost;\n    String? hostname;\n    String? user;\n    int port = 22;\n    String? identityFile;\n    String? jumpHost;\n\n    void addServer() {\n      if (currentHost != null && currentHost != '*' && hostname != null) {\n        final spi = Spi(\n          id: ShortId.generate(),\n          name: currentHost,\n          ip: hostname,\n          port: port,\n          user: user ?? 'root', // Default user is 'root'\n          keyId: identityFile,\n          jumpId: jumpHost,\n        );\n        servers.add(spi);\n      }\n    }\n\n    for (final line in lines) {\n      final trimmed = line.trim();\n      if (trimmed.isEmpty || trimmed.startsWith('#')) continue;\n\n      // Handle inline comments\n      final commentIndex = trimmed.indexOf('#');\n      final cleanLine = commentIndex != -1 ? trimmed.substring(0, commentIndex).trim() : trimmed;\n      if (cleanLine.isEmpty) continue;\n\n      final parts = cleanLine.split(RegExp(r'\\s+'));\n      if (parts.length < 2) continue;\n\n      final key = parts[0].toLowerCase();\n      var value = parts.sublist(1).join(' ');\n\n      // Remove quotes from values\n      if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n        value = value.substring(1, value.length - 1);\n      }\n\n      switch (key) {\n        case 'host':\n          // Save previous host config\n          addServer();\n\n          // Reset for new host\n          final originalValue = parts.sublist(1).join(' ');\n          final isQuoted =\n              (originalValue.startsWith('\"') && originalValue.endsWith('\"')) ||\n              (originalValue.startsWith(\"'\") && originalValue.endsWith(\"'\"));\n\n          currentHost = value;\n          // Skip hosts with multiple patterns (contains spaces but not quoted)\n          if (currentHost.contains(' ') && !isQuoted) {\n            currentHost = null; // Mark as invalid to skip\n          }\n          hostname = null;\n          user = null;\n          port = 22;\n          identityFile = null;\n          jumpHost = null;\n          break;\n\n        case 'hostname':\n          hostname = value;\n          break;\n\n        case 'user':\n          user = value;\n          break;\n\n        case 'port':\n          port = int.tryParse(value) ?? 22;\n          break;\n\n        case 'identityfile':\n          identityFile = value; // Store the path directly\n          break;\n\n        case 'proxyjump':\n        case 'proxycommand':\n          jumpHost = _extractJumpHost(value);\n          break;\n      }\n    }\n\n    // Add the last server\n    addServer();\n\n    return servers;\n  }\n\n  /// Extract jump host from ProxyJump or ProxyCommand\n  static String? _extractJumpHost(String value) {\n    if (value.isEmpty) return null;\n    // For ProxyJump, the format is usually: user@host:port\n    // For ProxyCommand, it's more complex and might need custom parsing\n    if (value.contains('@')) {\n      final parts = value.split(' ');\n      return parts.isNotEmpty ? parts[0] : null;\n    }\n    return null;\n  }\n\n  /// Check if SSH config file exists, trying multiple possible paths\n  static (File?, bool) configExists([String? configPath]) {\n    if (configPath != null) {\n      // If specific path is provided, use it directly\n      final homePath = _homePath;\n      if (homePath == null) {\n        Loggers.app.warning('Cannot determine home directory for SSH config parsing.');\n        return (null, false);\n      }\n      final expandedPath = configPath.replaceFirst('~', homePath);\n      dprint('Checking SSH config at path: $expandedPath');\n      final file = File(expandedPath);\n      return (file, file.existsSync());\n    }\n\n    // Try multiple possible paths\n    for (final path in _possibleConfigPaths) {\n      dprint('Checking SSH config at path: $path');\n      final file = File(path);\n      if (file.existsSync()) {\n        dprint('Found SSH config at: $path');\n        return (file, true);\n      }\n    }\n\n    dprint('SSH config file not found in any of the expected locations');\n    return (null, false);\n  }\n}\n"
  },
  {
    "path": "lib/data/helper/ssh_decoder.dart",
    "content": "import 'dart:convert';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter_gbk2utf8/flutter_gbk2utf8.dart';\n\n/// Utility class for decoding SSH command output with encoding fallback\nclass SSHDecoder {\n  /// Decodes bytes to string with multiple encoding fallback strategies\n  ///\n  /// Tries in order:\n  /// 1. UTF-8 (with allowMalformed for lenient parsing)\n  ///    - Windows PowerShell scripts now set UTF-8 output encoding by default\n  /// 2. GBK (for Windows Chinese systems)\n  ///    - In some cases, Windows will still revert to GBK.\n  ///    - Only attempted if UTF-8 produces replacement characters (�)\n  static String decode(\n    List<int> bytes, {\n    bool isWindows = false,\n    String? context,\n  }) {\n    if (bytes.isEmpty) return '';\n\n    // Try UTF-8 first with allowMalformed\n    try {\n      final result = utf8.decode(bytes, allowMalformed: true);\n      // Check if there are replacement characters indicating decode failure\n      // For non-Windows systems, always use UTF-8 result\n      if (!result.contains('�') || !isWindows) {\n        return result;\n      }\n      // For Windows with replacement chars, log and try GBK fallback\n      if (isWindows && result.contains('�')) {\n        final contextInfo = context != null ? ' [$context]' : '';\n        Loggers.app.info('UTF-8 decode has replacement chars$contextInfo, trying GBK fallback');\n      }\n    } catch (e) {\n      final contextInfo = context != null ? ' [$context]' : '';\n      Loggers.app.warning('UTF-8 decode failed$contextInfo: $e');\n    }\n\n    // For Windows or when UTF-8 has replacement chars, try GBK\n    try {\n      return gbk.decode(bytes);\n    } catch (e) {\n      final contextInfo = context != null ? ' [$context]' : '';\n      Loggers.app.warning('GBK decode failed$contextInfo: $e');\n      // Return empty string if all decoding attempts fail\n      return '';\n    }\n  }\n\n  /// Encodes string to bytes for SSH command input\n  ///\n  /// Uses GBK for Windows, UTF-8 for others\n  static List<int> encode(String text, {bool isWindows = false}) {\n    if (isWindows) {\n      try {\n        return gbk.encode(text);\n      } catch (e) {\n        Loggers.app.warning('GBK encode failed: $e, falling back to UTF-8');\n        return utf8.encode(text);\n      }\n    }\n    return utf8.encode(text);\n  }\n}\n"
  },
  {
    "path": "lib/data/helper/system_detector.dart",
    "content": "import 'package:dartssh2/dartssh2.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/core/extension/ssh_client.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/system.dart';\n\n/// Helper class for detecting remote system types\nclass SystemDetector {\n  /// Detects the system type of a remote server\n  ///\n  /// First checks if a custom system type is configured in [spi].\n  /// If not, attempts to detect the system by running commands:\n  /// 1. 'uname -a' command to detect Linux/BSD/Darwin\n  /// 2. 'ver' command to detect Windows (if uname fails)\n  ///\n  /// Returns [SystemType.linux] as default if detection fails.\n  static Future<SystemType> detect(SSHClient client, Spi spi) async {\n    // First, check if custom system type is defined\n    SystemType? detectedSystemType = spi.customSystemType;\n    if (detectedSystemType != null) {\n      dprint('Using custom system type ${detectedSystemType.name} for ${spi.oldId}');\n      return detectedSystemType;\n    }\n\n    try {\n      // Try to detect Unix/Linux/BSD systems first (more reliable and doesn't create files)\n      final unixResult = await client.runSafe(\n        'uname -a 2>/dev/null',\n        context: 'uname detection for ${spi.oldId}',\n      );\n      if (unixResult.contains('Linux')) {\n        detectedSystemType = SystemType.linux;\n        dprint('Detected Linux system type for ${spi.oldId}');\n        return detectedSystemType;\n      } else if (unixResult.contains('Darwin') || unixResult.contains('BSD')) {\n        detectedSystemType = SystemType.bsd;\n        dprint('Detected BSD system type for ${spi.oldId}');\n        return detectedSystemType;\n      }\n\n      // If uname fails, try to detect Windows systems\n      final powershellResult = await client.runSafe(\n        'ver 2>nul',\n        systemType: SystemType.windows,\n        context: 'ver detection for ${spi.oldId}',\n      );\n      if (powershellResult.isNotEmpty &&\n          (powershellResult.contains('Windows') || powershellResult.contains('NT'))) {\n        detectedSystemType = SystemType.windows;\n        dprint('Detected Windows system type for ${spi.oldId}');\n        return detectedSystemType;\n      }\n    } catch (e, stackTrace) {\n      Loggers.app.warning('System detection failed for ${spi.oldId}: $e\\n$stackTrace');\n    }\n\n    // Default fallback\n    detectedSystemType = SystemType.linux;\n    dprint('Defaulting to Linux system type for ${spi.oldId}');\n    return detectedSystemType;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/ai/ask_ai_models.dart",
    "content": "import 'package:meta/meta.dart';\n\n/// Chat message exchanged with the Ask AI service.\nenum AskAiMessageRole { user, assistant }\n\n@immutable\nclass AskAiMessage {\n  const AskAiMessage({\n    required this.role,\n    required this.content,\n  });\n\n  final AskAiMessageRole role;\n  final String content;\n\n  String get apiRole {\n    switch (role) {\n      case AskAiMessageRole.user:\n        return 'user';\n      case AskAiMessageRole.assistant:\n        return 'assistant';\n    }\n  }\n}\n\n/// Recommended command returned by the AI tool call.\n@immutable\nclass AskAiCommand {\n  const AskAiCommand({\n    required this.command,\n    this.description = '',\n    this.toolName,\n  });\n\n  final String command;\n  final String description;\n  final String? toolName;\n}\n\n@immutable\nsealed class AskAiEvent {\n  const AskAiEvent();\n}\n\n/// Incremental text delta emitted while streaming the AI response.\nclass AskAiContentDelta extends AskAiEvent {\n  const AskAiContentDelta(this.delta);\n  final String delta;\n}\n\n/// Emits when a tool call returns a runnable command suggestion.\nclass AskAiToolSuggestion extends AskAiEvent {\n  const AskAiToolSuggestion(this.command);\n  final AskAiCommand command;\n}\n\n/// Signals that the stream finished successfully.\nclass AskAiCompleted extends AskAiEvent {\n  const AskAiCompleted({\n    required this.fullText,\n    required this.commands,\n  });\n\n  final String fullText;\n  final List<AskAiCommand> commands;\n}\n\n/// Signals that the stream terminated with an error before completion.\nclass AskAiStreamError extends AskAiEvent {\n  const AskAiStreamError(this.error, this.stackTrace);\n\n  final Object error;\n  final StackTrace? stackTrace;\n}\n"
  },
  {
    "path": "lib/data/model/app/bak/backup.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:json_annotation/json_annotation.dart';\nimport 'package:logging/logging.dart';\nimport 'package:server_box/data/model/server/private_key_info.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/snippet.dart';\nimport 'package:server_box/data/res/misc.dart';\nimport 'package:server_box/data/res/store.dart';\n\npart 'backup.g.dart';\n\nconst backupFormatVersion = 1;\n\nfinal _logger = Logger('Backup');\n\n@JsonSerializable()\nclass Backup implements Mergeable {\n  // backup format version\n  final int version;\n  final String date;\n  final List<Spi> spis;\n  final List<Snippet> snippets;\n  final List<PrivateKeyInfo> keys;\n  final Map<String, dynamic> container;\n  final Map<String, dynamic> history;\n  final int? lastModTime;\n  final Map<String, dynamic>? settings;\n\n  const Backup({\n    required this.version,\n    required this.date,\n    required this.spis,\n    required this.snippets,\n    required this.keys,\n    required this.container,\n    required this.history,\n    required this.settings,\n    this.lastModTime,\n  });\n\n  factory Backup.fromJson(Map<String, dynamic> json) => _$BackupFromJson(json);\n\n  Map<String, dynamic> toJson() => _$BackupToJson(this);\n\n  static Future<Backup> loadFromStore() async {\n    final lastModTime = Stores.lastModTime;\n    return Backup(\n      version: backupFormatVersion,\n      date: DateTime.now().toString().split('.').firstOrNull ?? '',\n      spis: Stores.server.fetch(),\n      snippets: Stores.snippet.fetch(),\n      keys: Stores.key.fetch(),\n      container: Stores.container.getAllMap(),\n      lastModTime: lastModTime,\n      history: Stores.history.getAllMap(),\n      settings: Stores.setting.getAllMap(),\n    );\n  }\n\n  static Future<String> backup([String? name]) async {\n    final bak = await Backup.loadFromStore();\n    final result = _diyEncrypt(json.encode(bak.toJson()));\n    final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);\n    await File(path).writeAsString(result);\n    return path;\n  }\n\n  @override\n  Future<void> merge({bool force = false}) async {\n    final curTime = Stores.lastModTime;\n    final bakTime = lastModTime ?? 0;\n    final shouldRestore = force || curTime < bakTime;\n    if (!shouldRestore) {\n      _logger.info('No need to restore, local is newer');\n      return;\n    }\n\n    // Snippets\n    if (force) {\n      for (final s in snippets) {\n        Stores.snippet.box.put(s.name, s);\n      }\n    } else {\n      final nowSnippets = Stores.snippet.box.keys.toSet();\n      final bakSnippets = snippets.map((e) => e.name).toSet();\n      final newSnippets = bakSnippets.difference(nowSnippets);\n      final delSnippets = nowSnippets.difference(bakSnippets);\n      final updateSnippets = nowSnippets.intersection(bakSnippets);\n      for (final s in newSnippets) {\n        Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));\n      }\n      for (final s in delSnippets) {\n        Stores.snippet.box.delete(s);\n      }\n      for (final s in updateSnippets) {\n        Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));\n      }\n    }\n\n    // ServerPrivateInfo\n    if (force) {\n      for (final s in spis) {\n        Stores.server.box.put(s.id, s);\n      }\n    } else {\n      final nowSpis = Stores.server.box.keys.toSet();\n      final bakSpis = spis.map((e) => e.id).toSet();\n      final newSpis = bakSpis.difference(nowSpis);\n      final delSpis = nowSpis.difference(bakSpis);\n      final updateSpis = nowSpis.intersection(bakSpis);\n      for (final s in newSpis) {\n        Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));\n      }\n      for (final s in delSpis) {\n        Stores.server.box.delete(s);\n      }\n      for (final s in updateSpis) {\n        Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));\n      }\n    }\n\n    // PrivateKeyInfo\n    if (force) {\n      for (final s in keys) {\n        Stores.key.box.put(s.id, s);\n      }\n    } else {\n      final nowKeys = Stores.key.box.keys.toSet();\n      final bakKeys = keys.map((e) => e.id).toSet();\n      final newKeys = bakKeys.difference(nowKeys);\n      final delKeys = nowKeys.difference(bakKeys);\n      final updateKeys = nowKeys.intersection(bakKeys);\n      for (final s in newKeys) {\n        Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));\n      }\n      for (final s in delKeys) {\n        Stores.key.box.delete(s);\n      }\n      for (final s in updateKeys) {\n        Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));\n      }\n    }\n\n    // History\n    if (force) {\n      Stores.history.box.putAll(history);\n    } else {\n      final nowHistory = Stores.history.box.keys.toSet();\n      final bakHistory = history.keys.toSet();\n      final newHistory = bakHistory.difference(nowHistory);\n      final delHistory = nowHistory.difference(bakHistory);\n      final updateHistory = nowHistory.intersection(bakHistory);\n      for (final s in newHistory) {\n        Stores.history.box.put(s, history[s]);\n      }\n      for (final s in delHistory) {\n        Stores.history.box.delete(s);\n      }\n      for (final s in updateHistory) {\n        Stores.history.box.put(s, history[s]);\n      }\n    }\n\n    // Container\n    if (force) {\n      Stores.container.box.putAll(container);\n    } else {\n      final nowContainer = Stores.container.box.keys.toSet();\n      final bakContainer = container.keys.toSet();\n      final newContainer = bakContainer.difference(nowContainer);\n      final delContainer = nowContainer.difference(bakContainer);\n      final updateContainer = nowContainer.intersection(bakContainer);\n      for (final s in newContainer) {\n        Stores.container.box.put(s, container[s]);\n      }\n      for (final s in delContainer) {\n        Stores.container.box.delete(s);\n      }\n      for (final s in updateContainer) {\n        Stores.container.box.put(s, container[s]);\n      }\n    }\n\n    // Settings\n    final settings_ = settings;\n    if (settings_ != null) {\n      if (force) {\n        Stores.setting.box.putAll(settings_);\n      } else {\n        final nowSettings = Stores.setting.box.keys.toSet();\n        final bakSettings = settings_.keys.toSet();\n        final newSettings = bakSettings.difference(nowSettings);\n        final delSettings = nowSettings.difference(bakSettings);\n        final updateSettings = nowSettings.intersection(bakSettings);\n        for (final s in newSettings) {\n          Stores.setting.box.put(s, settings_[s]);\n        }\n        for (final s in delSettings) {\n          Stores.setting.box.delete(s);\n        }\n        for (final s in updateSettings) {\n          Stores.setting.box.put(s, settings_[s]);\n        }\n      }\n    }\n\n    Provider.reload();\n    RNodes.app.notify();\n\n    _logger.info('Restore success');\n  }\n\n  factory Backup.fromJsonString(String raw) => Backup.fromJson(json.decode(_diyDecrypt(raw)));\n}\n\nString _diyEncrypt(String raw) => json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));\n\nString _diyDecrypt(String raw) {\n  try {\n    final list = json.decode(raw);\n    final sb = StringBuffer();\n    for (final e in list) {\n      sb.writeCharCode((e - 1) ~/ 2);\n    }\n    return sb.toString();\n  } catch (e, trace) {\n    Loggers.app.warning('Backup decrypt failed', e, trace);\n    rethrow;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/bak/backup.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'backup.dart';\n\n// **************************************************************************\n// JsonSerializableGenerator\n// **************************************************************************\n\nBackup _$BackupFromJson(Map<String, dynamic> json) => Backup(\n  version: (json['version'] as num).toInt(),\n  date: json['date'] as String,\n  spis: (json['spis'] as List<dynamic>)\n      .map((e) => Spi.fromJson(e as Map<String, dynamic>))\n      .toList(),\n  snippets: (json['snippets'] as List<dynamic>)\n      .map((e) => Snippet.fromJson(e as Map<String, dynamic>))\n      .toList(),\n  keys: (json['keys'] as List<dynamic>)\n      .map((e) => PrivateKeyInfo.fromJson(e as Map<String, dynamic>))\n      .toList(),\n  container: json['container'] as Map<String, dynamic>,\n  history: json['history'] as Map<String, dynamic>,\n  settings: json['settings'] as Map<String, dynamic>?,\n  lastModTime: (json['lastModTime'] as num?)?.toInt(),\n);\n\nMap<String, dynamic> _$BackupToJson(Backup instance) => <String, dynamic>{\n  'version': instance.version,\n  'date': instance.date,\n  'spis': instance.spis,\n  'snippets': instance.snippets,\n  'keys': instance.keys,\n  'container': instance.container,\n  'history': instance.history,\n  'lastModTime': instance.lastModTime,\n  'settings': instance.settings,\n};\n"
  },
  {
    "path": "lib/data/model/app/bak/backup2.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:logging/logging.dart';\nimport 'package:server_box/data/provider/private_key.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/provider/snippet.dart';\nimport 'package:server_box/data/res/misc.dart';\nimport 'package:server_box/data/res/store.dart';\n\npart 'backup2.freezed.dart';\npart 'backup2.g.dart';\n\nfinal _loggerV2 = Logger('BackupV2');\n\n@freezed\nabstract class BackupV2 with _$BackupV2 implements Mergeable {\n  const BackupV2._();\n\n  /// Construct a backup with the latest format (v2).\n  ///\n  /// All `Map<String, dynamic>` are:\n  /// ```json\n  /// {\n  ///   \"key1\": Model{},\n  ///   \"_lastModTime\": {\n  ///     \"key1\": 1234567890,\n  ///   },\n  /// }\n  /// ```\n  const factory BackupV2({\n    required int version,\n    required int date,\n    required Map<String, Object?> spis,\n    required Map<String, Object?> snippets,\n    required Map<String, Object?> keys,\n    required Map<String, Object?> container,\n    required Map<String, Object?> history,\n    required Map<String, Object?> settings,\n  }) = _BackupV2;\n\n  factory BackupV2.fromJson(Map<String, dynamic> json) => _$BackupV2FromJson(json);\n\n  @override\n  Future<void> merge({bool force = false}) async {\n    _loggerV2.info('Merging...');\n\n    // Merge each store and check if changes were made\n    final serverChanged = await Mergeable.mergeStore(backupData: spis, store: Stores.server, force: force);\n    final snippetChanged = await Mergeable.mergeStore(backupData: snippets, store: Stores.snippet, force: force);\n    final keyChanged = await Mergeable.mergeStore(backupData: keys, store: Stores.key, force: force);\n    await Mergeable.mergeStore(backupData: container, store: Stores.container, force: force);\n    await Mergeable.mergeStore(backupData: history, store: Stores.history, force: force);\n    await Mergeable.mergeStore(backupData: settings, store: Stores.setting, force: force);\n\n    if (serverChanged) GlobalRef.gRef?.read(serversProvider.notifier).reload();\n    if (snippetChanged) GlobalRef.gRef?.read(snippetProvider.notifier).reload();\n    if (keyChanged) GlobalRef.gRef?.read(privateKeyProvider.notifier).reload();\n\n    _loggerV2.info('Merge completed');\n  }\n\n  static const formatVer = 2;\n\n  static Future<BackupV2> loadFromStore() async {\n    return BackupV2(\n      version: formatVer,\n      date: DateTimeX.timestamp,\n      spis: Stores.server.getAllMap(includeInternalKeys: true),\n      snippets: Stores.snippet.getAllMap(includeInternalKeys: true),\n      keys: Stores.key.getAllMap(includeInternalKeys: true),\n      container: Stores.container.getAllMap(includeInternalKeys: true),\n      history: Stores.history.getAllMap(includeInternalKeys: true),\n      settings: Stores.setting.getAllMap(includeInternalKeys: true),\n    );\n  }\n\n  static Future<String> backup([String? name, String? password]) async {\n    final bak = await BackupV2.loadFromStore();\n    var result = json.encode(bak.toJson());\n\n    if (password != null && password.isNotEmpty) {\n      result = Cryptor.encrypt(result, password);\n    }\n\n    final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);\n    await File(path).writeAsString(result);\n    return path;\n  }\n\n  factory BackupV2.fromJsonString(String jsonString, [String? password]) {\n    if (Cryptor.isEncrypted(jsonString)) {\n      if (password == null || password.isEmpty) {\n        throw Exception('Backup is encrypted but no password provided');\n      }\n      jsonString = Cryptor.decrypt(jsonString, password);\n    }\n\n    final map = json.decode(jsonString) as Map<String, dynamic>;\n    return BackupV2.fromJson(map);\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/bak/backup2.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'backup2.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n\n/// @nodoc\nmixin _$BackupV2 {\n\n int get version; int get date; Map<String, Object?> get spis; Map<String, Object?> get snippets; Map<String, Object?> get keys; Map<String, Object?> get container; Map<String, Object?> get history; Map<String, Object?> get settings;\n/// Create a copy of BackupV2\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$BackupV2CopyWith<BackupV2> get copyWith => _$BackupV2CopyWithImpl<BackupV2>(this as BackupV2, _$identity);\n\n  /// Serializes this BackupV2 to a JSON map.\n  Map<String, dynamic> toJson();\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is BackupV2&&(identical(other.version, version) || other.version == version)&&(identical(other.date, date) || other.date == date)&&const DeepCollectionEquality().equals(other.spis, spis)&&const DeepCollectionEquality().equals(other.snippets, snippets)&&const DeepCollectionEquality().equals(other.keys, keys)&&const DeepCollectionEquality().equals(other.container, container)&&const DeepCollectionEquality().equals(other.history, history)&&const DeepCollectionEquality().equals(other.settings, settings));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,version,date,const DeepCollectionEquality().hash(spis),const DeepCollectionEquality().hash(snippets),const DeepCollectionEquality().hash(keys),const DeepCollectionEquality().hash(container),const DeepCollectionEquality().hash(history),const DeepCollectionEquality().hash(settings));\n\n@override\nString toString() {\n  return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $BackupV2CopyWith<$Res>  {\n  factory $BackupV2CopyWith(BackupV2 value, $Res Function(BackupV2) _then) = _$BackupV2CopyWithImpl;\n@useResult\n$Res call({\n int version, int date, Map<String, Object?> spis, Map<String, Object?> snippets, Map<String, Object?> keys, Map<String, Object?> container, Map<String, Object?> history, Map<String, Object?> settings\n});\n\n\n\n\n}\n/// @nodoc\nclass _$BackupV2CopyWithImpl<$Res>\n    implements $BackupV2CopyWith<$Res> {\n  _$BackupV2CopyWithImpl(this._self, this._then);\n\n  final BackupV2 _self;\n  final $Res Function(BackupV2) _then;\n\n/// Create a copy of BackupV2\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? date = null,Object? spis = null,Object? snippets = null,Object? keys = null,Object? container = null,Object? history = null,Object? settings = null,}) {\n  return _then(_self.copyWith(\nversion: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable\nas int,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable\nas int,spis: null == spis ? _self.spis : spis // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,snippets: null == snippets ? _self.snippets : snippets // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,keys: null == keys ? _self.keys : keys // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,container: null == container ? _self.container : container // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,history: null == history ? _self.history : history // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,settings: null == settings ? _self.settings : settings // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [BackupV2].\nextension BackupV2Patterns on BackupV2 {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _BackupV2 value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _BackupV2() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _BackupV2 value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _BackupV2():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _BackupV2 value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _BackupV2() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int version,  int date,  Map<String, Object?> spis,  Map<String, Object?> snippets,  Map<String, Object?> keys,  Map<String, Object?> container,  Map<String, Object?> history,  Map<String, Object?> settings)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _BackupV2() when $default != null:\nreturn $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_that.container,_that.history,_that.settings);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int version,  int date,  Map<String, Object?> spis,  Map<String, Object?> snippets,  Map<String, Object?> keys,  Map<String, Object?> container,  Map<String, Object?> history,  Map<String, Object?> settings)  $default,) {final _that = this;\nswitch (_that) {\ncase _BackupV2():\nreturn $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_that.container,_that.history,_that.settings);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int version,  int date,  Map<String, Object?> spis,  Map<String, Object?> snippets,  Map<String, Object?> keys,  Map<String, Object?> container,  Map<String, Object?> history,  Map<String, Object?> settings)?  $default,) {final _that = this;\nswitch (_that) {\ncase _BackupV2() when $default != null:\nreturn $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_that.container,_that.history,_that.settings);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n@JsonSerializable()\n\nclass _BackupV2 extends BackupV2 {\n  const _BackupV2({required this.version, required this.date, required final  Map<String, Object?> spis, required final  Map<String, Object?> snippets, required final  Map<String, Object?> keys, required final  Map<String, Object?> container, required final  Map<String, Object?> history, required final  Map<String, Object?> settings}): _spis = spis,_snippets = snippets,_keys = keys,_container = container,_history = history,_settings = settings,super._();\n  factory _BackupV2.fromJson(Map<String, dynamic> json) => _$BackupV2FromJson(json);\n\n@override final  int version;\n@override final  int date;\n final  Map<String, Object?> _spis;\n@override Map<String, Object?> get spis {\n  if (_spis is EqualUnmodifiableMapView) return _spis;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableMapView(_spis);\n}\n\n final  Map<String, Object?> _snippets;\n@override Map<String, Object?> get snippets {\n  if (_snippets is EqualUnmodifiableMapView) return _snippets;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableMapView(_snippets);\n}\n\n final  Map<String, Object?> _keys;\n@override Map<String, Object?> get keys {\n  if (_keys is EqualUnmodifiableMapView) return _keys;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableMapView(_keys);\n}\n\n final  Map<String, Object?> _container;\n@override Map<String, Object?> get container {\n  if (_container is EqualUnmodifiableMapView) return _container;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableMapView(_container);\n}\n\n final  Map<String, Object?> _history;\n@override Map<String, Object?> get history {\n  if (_history is EqualUnmodifiableMapView) return _history;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableMapView(_history);\n}\n\n final  Map<String, Object?> _settings;\n@override Map<String, Object?> get settings {\n  if (_settings is EqualUnmodifiableMapView) return _settings;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableMapView(_settings);\n}\n\n\n/// Create a copy of BackupV2\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$BackupV2CopyWith<_BackupV2> get copyWith => __$BackupV2CopyWithImpl<_BackupV2>(this, _$identity);\n\n@override\nMap<String, dynamic> toJson() {\n  return _$BackupV2ToJson(this, );\n}\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackupV2&&(identical(other.version, version) || other.version == version)&&(identical(other.date, date) || other.date == date)&&const DeepCollectionEquality().equals(other._spis, _spis)&&const DeepCollectionEquality().equals(other._snippets, _snippets)&&const DeepCollectionEquality().equals(other._keys, _keys)&&const DeepCollectionEquality().equals(other._container, _container)&&const DeepCollectionEquality().equals(other._history, _history)&&const DeepCollectionEquality().equals(other._settings, _settings));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,version,date,const DeepCollectionEquality().hash(_spis),const DeepCollectionEquality().hash(_snippets),const DeepCollectionEquality().hash(_keys),const DeepCollectionEquality().hash(_container),const DeepCollectionEquality().hash(_history),const DeepCollectionEquality().hash(_settings));\n\n@override\nString toString() {\n  return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$BackupV2CopyWith<$Res> implements $BackupV2CopyWith<$Res> {\n  factory _$BackupV2CopyWith(_BackupV2 value, $Res Function(_BackupV2) _then) = __$BackupV2CopyWithImpl;\n@override @useResult\n$Res call({\n int version, int date, Map<String, Object?> spis, Map<String, Object?> snippets, Map<String, Object?> keys, Map<String, Object?> container, Map<String, Object?> history, Map<String, Object?> settings\n});\n\n\n\n\n}\n/// @nodoc\nclass __$BackupV2CopyWithImpl<$Res>\n    implements _$BackupV2CopyWith<$Res> {\n  __$BackupV2CopyWithImpl(this._self, this._then);\n\n  final _BackupV2 _self;\n  final $Res Function(_BackupV2) _then;\n\n/// Create a copy of BackupV2\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? date = null,Object? spis = null,Object? snippets = null,Object? keys = null,Object? container = null,Object? history = null,Object? settings = null,}) {\n  return _then(_BackupV2(\nversion: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable\nas int,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable\nas int,spis: null == spis ? _self._spis : spis // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,snippets: null == snippets ? _self._snippets : snippets // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,keys: null == keys ? _self._keys : keys // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,container: null == container ? _self._container : container // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,history: null == history ? _self._history : history // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,settings: null == settings ? _self._settings : settings // ignore: cast_nullable_to_non_nullable\nas Map<String, Object?>,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/model/app/bak/backup2.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'backup2.dart';\n\n// **************************************************************************\n// JsonSerializableGenerator\n// **************************************************************************\n\n_BackupV2 _$BackupV2FromJson(Map<String, dynamic> json) => _BackupV2(\n  version: (json['version'] as num).toInt(),\n  date: (json['date'] as num).toInt(),\n  spis: json['spis'] as Map<String, dynamic>,\n  snippets: json['snippets'] as Map<String, dynamic>,\n  keys: json['keys'] as Map<String, dynamic>,\n  container: json['container'] as Map<String, dynamic>,\n  history: json['history'] as Map<String, dynamic>,\n  settings: json['settings'] as Map<String, dynamic>,\n);\n\nMap<String, dynamic> _$BackupV2ToJson(_BackupV2 instance) => <String, dynamic>{\n  'version': instance.version,\n  'date': instance.date,\n  'spis': instance.spis,\n  'snippets': instance.snippets,\n  'keys': instance.keys,\n  'container': instance.container,\n  'history': instance.history,\n  'settings': instance.settings,\n};\n"
  },
  {
    "path": "lib/data/model/app/bak/backup_service.dart",
    "content": "import 'package:computer/computer.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/app/bak/backup2.dart';\nimport 'package:server_box/data/model/app/bak/backup_source.dart';\nimport 'package:server_box/data/model/app/bak/utils.dart';\n\n/// Service class for handling backup operations\nclass BackupService {\n  /// Perform backup operation with the given source\n  static Future<void> backup(BuildContext context, BackupSource source) async {\n    try {\n      final saved = await SecureStoreProps.bakPwd.read();\n      final password = saved?.isEmpty == true ? null : saved;\n\n      final path = await BackupV2.backup(null, password?.isEmpty == true ? null : password);\n      await source.saveContent(path);\n\n      if (source is ClipboardBackupSource) {\n        context.showSnackBar(libL10n.success);\n      }\n    } catch (e, s) {\n      context.showErrDialog(e, s, libL10n.backup);\n    }\n  }\n\n  /// Perform restore operation with the given source\n  static Future<void> restore(BuildContext context, BackupSource source) async {\n    final text = await source.getContent();\n    if (text == null) {\n      // Show empty message for clipboard source\n      if (source is ClipboardBackupSource) {\n        context.showSnackBar(libL10n.empty);\n      }\n      return;\n    }\n\n    await restoreFromText(context, text);\n  }\n\n  /// Handle restore from text with decryption support\n  static Future<void> restoreFromText(BuildContext context, String text) async {\n    // Check if backup is encrypted\n    final isEncrypted = Cryptor.isEncrypted(text);\n    String? password;\n\n    if (!isEncrypted) {\n      try {\n        final (backup, err) = await context.showLoadingDialog(\n          fn: () => Computer.shared.start(MergeableUtils.fromJsonString, text),\n        );\n        if (err != null || backup == null) return;\n\n        await _confirmAndRestore(context, backup);\n      } catch (e, s) {\n        Loggers.app.warning('Import backup failed', e, s);\n        context.showErrDialog(e, s, libL10n.restore);\n      }\n      return;\n    }\n\n    // Try with saved password first\n    final savedPassword = await SecureStoreProps.bakPwd.read();\n    if (savedPassword != null && savedPassword.isNotEmpty) {\n      try {\n        final (backup, err) = await context.showLoadingDialog(\n          fn: () => Computer.shared.start((args) => MergeableUtils.fromJsonString(args.$1, args.$2), (\n            text,\n            savedPassword,\n          )),\n        );\n        if (err == null && backup != null) {\n          await _confirmAndRestore(context, backup);\n          return;\n        }\n      } catch (e, s) {\n        Loggers.app.warning('Failed to restore with saved password, will prompt for manual input', e, s);\n      }\n    }\n\n    // Prompt for password with retry logic\n    while (true) {\n      password = await _showPasswordDialog(context, title: libL10n.pwd, hint: l10n.backupEncrypted);\n      if (password == null) return; // User cancelled\n\n      try {\n        final (backup, err) = await context.showLoadingDialog(\n          fn: () => Computer.shared.start((args) => MergeableUtils.fromJsonString(args.$1, args.$2), (\n            text,\n            password,\n          )),\n        );\n        if (err != null || backup == null) continue;\n\n        await _confirmAndRestore(context, backup);\n        return;\n      } catch (e) {\n        if (e.toString().contains('incorrect password') || e.toString().contains('Failed to decrypt')) {\n          final retry = await context.showRoundDialog<bool>(\n            title: l10n.backupPasswordWrong,\n            child: Text(l10n.backupPasswordWrong),\n            actions: [\n              TextButton(onPressed: () => context.pop(false), child: Text(libL10n.cancel)),\n              TextButton(onPressed: () => context.pop(true), child: Text(libL10n.retry)),\n            ],\n          );\n          if (retry != true) return;\n          continue; // Try again\n        } else {\n          // Other error, show and exit\n          context.showErrDialog(e, null, libL10n.restore);\n          return;\n        }\n      }\n    }\n  }\n\n  /// Confirm and execute restore operation\n  static Future<void> _confirmAndRestore(BuildContext context, (dynamic, String) backup) async {\n    await context.showRoundDialog(\n      title: libL10n.restore,\n      child: Text(libL10n.askContinue('${libL10n.restore} ${libL10n.backup}(${backup.$2})')),\n      actions: [\n        Btn.cancel(),\n        Btn.ok(\n          onTap: () async {\n            try {\n              await backup.$1.merge(force: true);\n              context.pop();\n            } catch (e, s) {\n              context.pop();\n              context.showErrDialog(e, s, libL10n.restore);\n            }\n          },\n        ),\n      ],\n    );\n  }\n\n  /// Show password input dialog\n  static Future<String?> _showPasswordDialog(\n    BuildContext context, {\n    String? initial,\n    String? title,\n    String? hint,\n  }) async {\n    final controller = TextEditingController(text: initial ?? '');\n    final result = await context.showRoundDialog<String>(\n      title: title ?? libL10n.pwd,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Text(hint ?? l10n.backupPasswordTip, style: UIs.textGrey),\n          UIs.height13,\n          Input(\n            label: l10n.backupPassword,\n            controller: controller,\n            obscureText: true,\n            onSubmitted: (_) => context.pop(controller.text),\n          ),\n        ],\n      ),\n      actions: [\n        Btn.cancel(),\n        TextButton(onPressed: () => context.pop(controller.text), child: Text(libL10n.ok)),\n      ],\n    );\n    controller.dispose();\n    return result;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/bak/backup_source.dart",
    "content": "import 'dart:io';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\n\n/// Abstract interface for backup content sources\nabstract class BackupSource {\n  /// Get content from this source for restore\n  Future<String?> getContent();\n\n  /// Save content to this source for backup\n  Future<void> saveContent(String filePath);\n\n  /// Display name for this source\n  String get displayName;\n\n  /// Icon for this source\n  IconData get icon;\n}\n\n/// File-based backup source\nclass FileBackupSource implements BackupSource {\n  @override\n  Future<String?> getContent() async {\n    return await Pfs.pickFileString();\n  }\n\n  @override\n  Future<void> saveContent(String filePath) async {\n    await Pfs.sharePaths(paths: [filePath]);\n  }\n\n  @override\n  String get displayName => libL10n.file;\n\n  @override\n  IconData get icon => Icons.file_open;\n}\n\n/// Clipboard-based backup source\nclass ClipboardBackupSource implements BackupSource {\n  @override\n  Future<String?> getContent() async {\n    final text = await Pfs.paste();\n    if (text == null || text.isEmpty) {\n      return null;\n    }\n    return text.trim();\n  }\n\n  @override\n  Future<void> saveContent(String filePath) async {\n    final content = await File(filePath).readAsString();\n    Pfs.copy(content);\n  }\n\n  @override\n  String get displayName => libL10n.clipboard;\n\n  @override\n  IconData get icon => Icons.content_paste;\n}\n"
  },
  {
    "path": "lib/data/model/app/bak/utils.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/app/bak/backup.dart';\nimport 'package:server_box/data/model/app/bak/backup2.dart';\n\nabstract final class MergeableUtils {\n  static (Mergeable, String) fromJsonString(String json, [String? password]) {\n    try {\n      final bak = BackupV2.fromJsonString(json, password);\n      return (bak, DateTime.fromMillisecondsSinceEpoch(bak.date).hms());\n    } catch (e) {\n      final bak = Backup.fromJsonString(json);\n      return (bak, bak.date);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/error.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\n\nenum SSHErrType { unknown, connect, auth, noPrivateKey, chdir, segements, writeScript, getStatus }\n\nclass SSHErr extends Err<SSHErrType> {\n  const SSHErr({required super.type, super.message});\n\n  @override\n  String? get solution => switch (type) {\n    SSHErrType.chdir => l10n.needHomeDir,\n    SSHErrType.auth => l10n.authFailTip,\n    SSHErrType.writeScript => l10n.writeScriptFailTip,\n    SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,\n    _ => null,\n  };\n}\n\nenum ContainerErrType {\n  unknown,\n  noClient,\n  notInstalled,\n  invalidVersion,\n  cmdNoPrefix,\n  segmentsNotMatch,\n  parsePs,\n  parseImages,\n  parseStats,\n  podmanDetected,\n  sudoPasswordRequired,\n  sudoPasswordIncorrect,\n}\n\nclass ContainerErr extends Err<ContainerErrType> {\n  const ContainerErr({required super.type, super.message});\n\n  @override\n  String? get solution => null;\n}\n\nenum ICloudErrType { generic, notFound, multipleFiles }\n\nclass ICloudErr extends Err<ICloudErrType> {\n  const ICloudErr({required super.type, super.message});\n\n  @override\n  String? get solution => null;\n}\n\nenum WebdavErrType { generic, notFound }\n\nclass WebdavErr extends Err<WebdavErrType> {\n  const WebdavErr({required super.type, super.message});\n\n  @override\n  String? get solution => null;\n}\n\nenum PveErrType { unknown, net, loginFailed }\n\nclass PveErr extends Err<PveErrType> {\n  const PveErr({required super.type, super.message});\n\n  @override\n  String? get solution => null;\n}\n"
  },
  {
    "path": "lib/data/model/app/menu/base.dart",
    "content": "import 'package:flutter/material.dart';\n\nabstract class PopMenu {\n  static PopupMenuItem<T> build<T>(\n    T t,\n    IconData icon,\n    String text, {\n    double? iconSize,\n  }) {\n    return PopupMenuItem<T>(\n      value: t,\n      child: Row(\n        children: [\n          Icon(icon, size: iconSize),\n          const SizedBox(width: 10),\n          Text(text),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/menu/container.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\n\nenum ContainerMenu {\n  start,\n  stop,\n  restart,\n  rm,\n  logs,\n  terminal\n  //stats,\n  ;\n\n  static List<ContainerMenu> items(bool running) {\n    if (running) {\n      return [\n        stop,\n        restart,\n        rm,\n        logs,\n        terminal,\n        //stats,\n      ];\n    }\n    return [start, rm, logs];\n  }\n\n  IconData get icon => switch (this) {\n    ContainerMenu.start => Icons.play_arrow,\n    ContainerMenu.stop => Icons.stop,\n    ContainerMenu.restart => Icons.restart_alt,\n    ContainerMenu.rm => Icons.delete,\n    ContainerMenu.logs => Icons.logo_dev,\n    ContainerMenu.terminal => Icons.terminal,\n    // DockerMenuType.stats => Icons.bar_chart,\n  };\n\n  String get toStr => switch (this) {\n    ContainerMenu.start => libL10n.start,\n    ContainerMenu.stop => libL10n.stop,\n    ContainerMenu.restart => libL10n.restart,\n    ContainerMenu.rm => libL10n.delete,\n    ContainerMenu.logs => libL10n.log,\n    ContainerMenu.terminal => libL10n.terminal,\n    // DockerMenuType.stats => s.stats,\n  };\n}\n"
  },
  {
    "path": "lib/data/model/app/menu/platform.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/app/tab.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/data/res/url.dart';\nimport 'package:server_box/generated/l10n/l10n.dart';\nimport 'package:server_box/view/page/setting/entry.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\n/// macOS Menu Bar\nclass MacOSMenuBarManager {\n  static List<PlatformMenu> buildMenuBar(BuildContext context, Function(int) onTabChanged) {\n    final l10n = context.l10n;\n    final homeTabs = Stores.setting.homeTabs.fetch();\n    return [\n      PlatformMenu(\n        label: 'Server Box',\n        menus: [\n          PlatformMenuItem(\n            label: libL10n.about,\n            onSelected: () => _showAboutDialog(context),\n          ),\n          PlatformMenuItem(\n            label: libL10n.menuSettings,\n            shortcut: const SingleActivator(LogicalKeyboardKey.comma, meta: true),\n            onSelected: () => _openSettings(context),\n          ),\n          PlatformMenuItem(\n            label: libL10n.menuQuit,\n            shortcut: const SingleActivator(LogicalKeyboardKey.keyQ, meta: true),\n            onSelected: () => SystemNavigator.pop(),\n          ),\n        ],\n      ),\n      PlatformMenu(\n        label: libL10n.menuNavigate,\n        menus: _buildNavigateMenuItems(l10n, homeTabs, onTabChanged),\n      ),\n      PlatformMenu(\n        label: libL10n.menuInfo,\n        menus: [\n          PlatformMenuItem(\n            label: l10n.menuGitHubRepository,\n            onSelected: () => _openURL(Urls.thisRepo),\n          ),\n          PlatformMenuItem(\n            label: libL10n.menuWiki,\n            onSelected: () => _openURL(Urls.appWiki),\n          ),\n          PlatformMenuItem(\n            label: libL10n.menuHelp,\n            onSelected: () => _openURL(Urls.appHelp),\n          ),\n        ],\n      ),\n    ];\n  }\n\n  static List<PlatformMenuItem> _buildNavigateMenuItems(\n    AppLocalizations l10n,\n    List<AppTab> homeTabs,\n    Function(int) onTabChanged,\n  ) {\n    final menuItems = <PlatformMenuItem>[];\n    final tabLabels = {\n      AppTab.server: libL10n.server,\n      AppTab.ssh: 'SSH',\n      AppTab.file: libL10n.file,\n      AppTab.snippet: libL10n.snippet,\n    };\n    for (var i = 0; i < homeTabs.length; i++) {\n      final tab = homeTabs[i];\n      final label = tabLabels[tab];\n      if (label == null) continue;\n      final shortcutKey = _getShortcutKeyForIndex(i);\n      menuItems.add(PlatformMenuItem(\n        label: label,\n        shortcut: shortcutKey != null\n            ? SingleActivator(shortcutKey, meta: true)\n            : null,\n        onSelected: () => onTabChanged(i),\n      ));\n    }\n    return menuItems;\n  }\n\n  static LogicalKeyboardKey? _getShortcutKeyForIndex(int index) {\n    const keys = [\n      LogicalKeyboardKey.digit1,\n      LogicalKeyboardKey.digit2,\n      LogicalKeyboardKey.digit3,\n      LogicalKeyboardKey.digit4,\n      LogicalKeyboardKey.digit5,\n      LogicalKeyboardKey.digit6,\n      LogicalKeyboardKey.digit7,\n      LogicalKeyboardKey.digit8,\n      LogicalKeyboardKey.digit9,\n    ];\n    return index < keys.length ? keys[index] : null;\n  }\n\n  static Future<void> _showAboutDialog(BuildContext context) async {\n    const channel = MethodChannel('about');\n    await channel.invokeMethod('showAboutPanel');\n  }\n\n  static void _openSettings(BuildContext context) {\n    SettingsPage.route.go(context);\n  }\n\n  static Future<void> _openURL(String url) async {\n    final uri = Uri.parse(url);\n    if (await canLaunchUrl(uri)) {\n      await launchUrl(uri);\n    }\n  }\n}"
  },
  {
    "path": "lib/data/model/app/menu/server_func.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/data/res/store.dart';\n\nenum ServerFuncBtn {\n  terminal(),\n  sftp(),\n  container(),\n  process(),\n  //pkg(),\n  snippet(),\n  iperf(),\n  // pve(),\n  systemd(1058);\n\n  final int? addedVersion;\n\n  const ServerFuncBtn([this.addedVersion]);\n\n  static void autoAddNewFuncs(int cur) {\n    if (cur >= systemd.addedVersion!) {\n      final prop = Stores.setting.serverFuncBtns;\n      final list = prop.fetch();\n      if (!list.contains(systemd.index)) {\n        list.add(systemd.index);\n        prop.put(list);\n      }\n    }\n  }\n\n  static final defaultIdxs = [\n    terminal,\n    sftp,\n    container,\n    process,\n    //pkg,\n    snippet,\n    systemd,\n  ].map((e) => e.index).toList();\n\n  IconData get icon => switch (this) {\n    sftp => Icons.insert_drive_file,\n    snippet => Icons.code,\n    //pkg => Icons.system_security_update,\n    container => FontAwesome.docker_brand,\n    process => Icons.list_alt_outlined,\n    terminal => Icons.terminal,\n    iperf => Icons.speed,\n    systemd => MingCute.plugin_2_fill,\n  };\n\n  String get toStr => switch (this) {\n    sftp => 'SFTP',\n    snippet => libL10n.snippet,\n    //pkg => libL10n.pkg,\n    container => libL10n.container,\n    process => libL10n.process,\n    terminal => libL10n.terminal,\n    iperf => 'iperf',\n    systemd => 'Systemd',\n  };\n}\n"
  },
  {
    "path": "lib/data/model/app/net_view.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/server/server.dart';\n\nenum NetViewType {\n  conn,\n  speed,\n  traffic;\n\n  NetViewType get next => switch (this) {\n    conn => speed,\n    speed => traffic,\n    traffic => conn,\n  };\n\n  String get toStr => switch (this) {\n    NetViewType.conn => libL10n.conn,\n    NetViewType.traffic => libL10n.traffic,\n    NetViewType.speed => l10n.speed,\n  };\n\n  /// If no device is specified, return the cached value (only real devices,\n  /// such as ethX, wlanX...).\n  (String, String) build(ServerStatus ss, {String? dev}) {\n    final notSepcifyDev = dev == null || dev.isEmpty;\n    try {\n      switch (this) {\n        case NetViewType.conn:\n          return ('${libL10n.conn}:\\n${ss.tcp.maxConn}', '${libL10n.fail}:\\n${ss.tcp.fail}');\n        case NetViewType.speed:\n          if (notSepcifyDev) {\n            return ('↓:\\n${ss.netSpeed.cachedVals.speedIn}', '↑:\\n${ss.netSpeed.cachedVals.speedOut}');\n          }\n          return ('↓:\\n${ss.netSpeed.speedIn(device: dev)}', '↑:\\n${ss.netSpeed.speedOut(device: dev)}');\n        case NetViewType.traffic:\n          if (notSepcifyDev) {\n            return ('↓:\\n${ss.netSpeed.cachedVals.sizeIn}', '↑:\\n${ss.netSpeed.cachedVals.sizeOut}');\n          }\n          return ('↓:\\n${ss.netSpeed.sizeIn(device: dev)}', '↑:\\n${ss.netSpeed.sizeOut(device: dev)}');\n      }\n    } catch (e, s) {\n      Loggers.app.warning('NetViewType.build', e, s);\n      return ('N/A', 'N/A');\n    }\n  }\n\n  int toJson() => switch (this) {\n    NetViewType.conn => 0,\n    NetViewType.speed => 1,\n    NetViewType.traffic => 2,\n  };\n\n  static NetViewType fromJson(int json) => switch (json) {\n    0 => NetViewType.conn,\n    1 => NetViewType.speed,\n    _ => NetViewType.traffic,\n  };\n}\n"
  },
  {
    "path": "lib/data/model/app/path_with_prefix.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\n\nfinal _seperator = Pfs.seperator;\n\n/// It's used on platform's file system.\n/// So use [Platform.pathSeparator] to join path.\nclass LocalPath {\n  final String _prefixPath;\n  String _path = _seperator;\n  String? _prePath;\n  String get path => _prefixPath + _path;\n\n  LocalPath(String prefixPath) : _prefixPath = _trimSuffix(prefixPath);\n\n  void update(String newPath) {\n    _prePath = _path;\n    if (newPath == '..') {\n      _path = _path.substring(0, _path.lastIndexOf(_seperator));\n      if (_path == '') {\n        _path = _seperator;\n      }\n      return;\n    }\n    if (newPath == _seperator) {\n      _path = _seperator;\n      return;\n    }\n    _path = _path.joinPath(newPath);\n  }\n\n  bool get canBack => path != '$_prefixPath$_seperator';\n\n  bool undo() {\n    if (_prePath == null || _path == _prePath) {\n      return false;\n    }\n    _path = _prePath!;\n    return true;\n  }\n}\n\nString _trimSuffix(String prefixPath) {\n  if (prefixPath.endsWith(_seperator)) {\n    return prefixPath.substring(0, prefixPath.length - 1);\n  }\n  return prefixPath;\n}\n"
  },
  {
    "path": "lib/data/model/app/range.dart",
    "content": "final class Range<T extends num> {\n  final T start;\n  final T end;\n\n  Range(this.start, this.end);\n\n  bool contains(int value) => value >= start && value <= end;\n\n  @override\n  String toString() => 'Range($start, $end)';\n}\n"
  },
  {
    "path": "lib/data/model/app/scripts/cmd_types.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/app/scripts/script_consts.dart';\nimport 'package:server_box/data/model/server/system.dart';\n\n/// Enum representing different command types for various systems\nenum CmdTypeSys {\n  linux('Linux'),\n  bsd('BSD'),\n  windows('Windows');\n\n  final String sign;\n  const CmdTypeSys(this.sign);\n\n  IconData get icon {\n    return switch (this) {\n      CmdTypeSys.linux => MingCute.linux_line,\n      CmdTypeSys.bsd => LineAwesome.freebsd,\n      CmdTypeSys.windows => MingCute.windows_line,\n    };\n  }\n}\n\n/// Base class for all command type enums\nsealed class ShellCmdType implements Enum {\n  String get cmd;\n\n  /// Get command-specific separator\n  String get separator;\n\n  /// Get command-specific divider (separator with echo and formatting)\n  String get divider;\n\n  /// Get corresponding system type\n  CmdTypeSys get sysType;\n\n  static Set<ShellCmdType> get all {\n    return {...StatusCmdType.values, ...BSDStatusCmdType.values, ...WindowsStatusCmdType.values};\n  }\n}\n\nextension ShellCmdTypeX on ShellCmdType {\n  /// Display name of the command type\n  String get displayName => '${sysType.sign}.$name';\n}\n\n/// Linux/Unix status commands\nenum StatusCmdType implements ShellCmdType {\n  echo('echo ${SystemType.linuxSign}'),\n  time('date +%s'),\n  net('cat /proc/net/dev'),\n  sys('cat /etc/*-release | grep ^PRETTY_NAME'),\n  cpu('cat /proc/stat | grep cpu'),\n  uptime('uptime'),\n  conn('cat /proc/net/snmp'),\n  disk(\n    '(lsblk --bytes --json --output '\n    'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID 2>/dev/null && echo \"LSBLK_SUCCESS\") || df -k'\n  ),\n  mem(\"cat /proc/meminfo | grep -E 'Mem|Swap'\"),\n  tempType('cat /sys/class/thermal/thermal_zone*/type'),\n  tempVal('cat /sys/class/thermal/thermal_zone*/temp'),\n  host('cat /etc/hostname'),\n  diskio('cat /proc/diskstats'),\n\n  /// Get battery information from Linux power supply subsystem\n  ///\n  /// Reads battery data from sysfs power supply interface:\n  /// - Iterates through all power supply devices in /sys/class/power_supply/\n  /// - Each device has a uevent file with key-value pairs of power supply properties\n  /// - Includes battery level, status, technology type, and other attributes\n  /// - Works with laptops, UPS devices, and other power supplies\n  /// - Adds echo after each file to separate multiple power supplies\n  /// - Returns empty if no power supplies are detected (e.g., desktop systems)\n  battery('for f in /sys/class/power_supply/*/uevent; do cat \"\\$f\"; echo; done'),\n\n  /// Get NVIDIA GPU information using nvidia-smi in XML format\n  /// Requires NVIDIA drivers and nvidia-smi utility to be installed\n  nvidia('nvidia-smi -q -x'),\n\n  /// Get AMD GPU information using multiple fallback methods\n  ///\n  /// This command tries three different AMD monitoring tools in order of preference:\n  /// 1. amd-smi: Modern AMD System Management Interface (ROCm 5.0+)\n  ///    - Uses 'amd-smi list --json' to get GPU list\n  ///    - Uses 'amd-smi metric --json' to get performance metrics\n  /// 2. rocm-smi: ROCm System Management Interface (older versions)\n  ///    - First tries '--json' output format if supported\n  ///    - Falls back to human-readable format with comprehensive metrics\n  /// 3. radeontop: Real-time GPU usage monitor for older AMD cards\n  ///    - Uses 2-second timeout to avoid hanging\n  ///    - Skips header line with 'tail -n +2'\n  ///    - Outputs single line of usage data\n  ///\n  /// If none of these tools are available, outputs error message\n  amd(\n    'if command -v amd-smi >/dev/null 2>&1; then '\n    'amd-smi list --json && amd-smi metric --json; '\n    'elif command -v rocm-smi >/dev/null 2>&1; then '\n    'rocm-smi --json || rocm-smi --showunique --showuse --showtemp '\n    '--showfan --showclocks --showmemuse --showpower; '\n    'elif command -v radeontop >/dev/null 2>&1; then '\n    'timeout 2s radeontop -d - -l 1 | tail -n +2; '\n    'else echo \"No AMD GPU monitoring tools found\"; fi',\n  ),\n  sensors('sensors'),\n\n  /// Get SMART disk health information for all storage devices\n  ///\n  /// Uses a combination of lsblk and smartctl to collect disk health data:\n  /// - lsblk -dn -o KNAME lists all block devices (kernel names only, no dependencies)\n  /// - For each device, runs smartctl with -a (all info) and -j (JSON output)\n  /// - Targets raw device nodes in /dev/ (e.g., /dev/sda, /dev/nvme0n1)\n  /// - Adds echo after each device to separate output blocks\n  /// - May require elevated privileges for some drives\n  /// - smartctl must be installed (part of smartmontools package)\n  diskSmart('for d in \\$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\\$d; echo; done'),\n  cpuBrand('cat /proc/cpuinfo | grep \"model name\"');\n\n  @override\n  final String cmd;\n\n  const StatusCmdType(this.cmd);\n\n  @override\n  String get separator => ScriptConstants.getCmdSeparator(name);\n\n  @override\n  String get divider => ScriptConstants.getCmdDivider(name);\n\n  @override\n  CmdTypeSys get sysType => CmdTypeSys.linux;\n}\n\n/// BSD/macOS status commands\nenum BSDStatusCmdType implements ShellCmdType {\n  echo('echo ${SystemType.bsdSign}'),\n  time('date +%s'),\n  net('netstat -ibn'),\n  sys('uname -or'),\n  cpu('top -l 1 | grep \"CPU usage\"'),\n  uptime('uptime'),\n  disk('df -k'), // Keep df -k for BSD systems as lsblk is not available on macOS/BSD\n  mem('top -l 1 | grep PhysMem'),\n  host('hostname'),\n  cpuBrand('sysctl -n machdep.cpu.brand_string');\n\n  @override\n  final String cmd;\n\n  const BSDStatusCmdType(this.cmd);\n\n  @override\n  String get separator => ScriptConstants.getCmdSeparator(name);\n\n  @override\n  String get divider => ScriptConstants.getCmdDivider(name);\n\n  @override\n  CmdTypeSys get sysType => CmdTypeSys.bsd;\n}\n\n/// Windows PowerShell status commands\nenum WindowsStatusCmdType implements ShellCmdType {\n  echo('echo ${SystemType.windowsSign}'),\n  time('[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()'),\n\n  /// Get network interface statistics using WMI\n  ///\n  /// Uses WMI Win32_PerfRawData_Tcpip_NetworkInterface for cross-language compatibility:\n  /// - Takes 2 samples with 1 second interval to calculate rates\n  net(\n    r'$s1 = @(Get-WmiObject Win32_PerfRawData_Tcpip_NetworkInterface | '\n    r'Select-Object Name, BytesReceivedPersec, BytesSentPersec, Timestamp_Sys100NS); '\n    r'Start-Sleep -Seconds 1; '\n    r'$s2 = @(Get-WmiObject Win32_PerfRawData_Tcpip_NetworkInterface | '\n    r'Select-Object Name, BytesReceivedPersec, BytesSentPersec, Timestamp_Sys100NS); '\n    r'@($s1, $s2) | ConvertTo-Json -Depth 5',\n  ),\n  sys('(Get-ComputerInfo).OsName'),\n  cpu(\n    'Get-WmiObject -Class Win32_Processor | '\n    'Select-Object Name, LoadPercentage, NumberOfCores, NumberOfLogicalProcessors | ConvertTo-Json',\n  ),\n\n  /// Get system uptime by calculating time since last boot\n  ///\n  /// Calculates uptime directly in PowerShell to avoid date format parsing issues:\n  /// - Gets LastBootUpTime from Win32_OperatingSystem\n  /// - Calculates difference from current time\n  /// - Returns pre-formatted string: \"X days, H:MM\" or \"H:MM\" (if less than 1 day)\n  /// - Uses ToString('00') for zero-padding to avoid quote escaping issues\n  uptime(\n    r\"\"\"$up = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime; if ($up.Days -gt 0) { \"$($up.Days) days, $($up.Hours):$($up.Minutes.ToString('00'))\" } else { \"$($up.Hours):$($up.Minutes.ToString('00'))\" }\"\"\",\n  ),\n  conn('(netstat -an | findstr ESTABLISHED | Measure-Object -Line).Count'),\n  disk(\n    'Get-WmiObject -Class Win32_LogicalDisk | '\n    'Select-Object DeviceID, Size, FreeSpace, FileSystem | ConvertTo-Json',\n  ),\n  mem(\n    'Get-WmiObject -Class Win32_OperatingSystem | '\n    'Select-Object TotalVisibleMemorySize, FreePhysicalMemory | ConvertTo-Json',\n  ),\n\n  /// Get system temperature using Windows Management Instrumentation (WMI)\n  ///\n  /// Queries the MSAcpi_ThermalZoneTemperature class from the WMI root/wmi namespace:\n  /// - Uses Get-CimInstance to access ACPI thermal zone data\n  /// - ErrorAction SilentlyContinue prevents errors on systems without thermal sensors\n  /// - Converts temperature from 10ths of Kelvin to Celsius: (temp - 2732) / 10\n  /// - Uses calculated property to perform the temperature conversion\n  /// - Returns JSON with InstanceName and converted Temperature values\n  /// - May return empty result on systems without ACPI thermal sensor support\n  temp(\n    'Get-CimInstance -ClassName MSAcpi_ThermalZoneTemperature '\n    '-Namespace root/wmi -ErrorAction SilentlyContinue | '\n    'Select-Object InstanceName, @{Name=\\'Temperature\\';'\n    'Expression={[math]::Round((\\$_.CurrentTemperature - 2732) / 10, 1)}} | '\n    'ConvertTo-Json',\n  ),\n  host(r'Write-Output $env:COMPUTERNAME'),\n\n  /// Get disk I/O statistics using WMI\n  ///\n  /// Uses WMI Win32_PerfRawData_PerfDisk_PhysicalDisk:\n  /// - Monitors read and write bytes per second for all physical disks\n  /// - Takes 2 samples with 1 second interval to calculate rates\n  /// - DiskReadBytesPersec and DiskWriteBytesPersec are cumulative counters\n  diskio(\n    r'$s1 = @(Get-WmiObject Win32_PerfRawData_PerfDisk_PhysicalDisk | '\n    r'Select-Object Name, DiskReadBytesPersec, DiskWriteBytesPersec, Timestamp_Sys100NS); '\n    r'Start-Sleep -Seconds 1; '\n    r'$s2 = @(Get-WmiObject Win32_PerfRawData_PerfDisk_PhysicalDisk | '\n    r'Select-Object Name, DiskReadBytesPersec, DiskWriteBytesPersec, Timestamp_Sys100NS); '\n    r'@($s1, $s2) | ConvertTo-Json -Depth 5',\n  ),\n  battery(\n    'Get-WmiObject -Class Win32_Battery | '\n    'Select-Object EstimatedChargeRemaining, BatteryStatus | ConvertTo-Json',\n  ),\n\n  /// Get NVIDIA GPU information on Windows\n  ///\n  /// Checks if nvidia-smi is available before attempting to use it:\n  /// - Uses Get-Command to test if nvidia-smi.exe exists in PATH\n  /// - ErrorAction SilentlyContinue prevents PowerShell errors if not found\n  /// - If available, runs nvidia-smi with -q (query) and -x (XML output) flags\n  /// - If not available, outputs standard error message for consistent handling\n  nvidia(\n    'if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) { '\n    'nvidia-smi -q -x } else { echo \"NVIDIA driver not found\" }',\n  ),\n\n  /// Get AMD GPU information on Windows\n  ///\n  /// Checks for AMD monitoring tools using similar pattern to Linux version:\n  /// - Uses Get-Command to test if amd-smi.exe exists in PATH\n  /// - ErrorAction SilentlyContinue prevents PowerShell errors if not found\n  /// - If available, runs amd-smi list command with JSON output\n  /// - If not available, outputs standard error message for consistent handling\n  /// - Windows version is simpler than Linux due to fewer AMD tool variations\n  amd(\n    'if (Get-Command amd-smi -ErrorAction SilentlyContinue) { '\n    'amd-smi list --json } else { echo \"AMD driver not found\" }',\n  ),\n  sensors(\n    'Get-CimInstance -ClassName Win32_TemperatureProbe '\n    '-ErrorAction SilentlyContinue | '\n    'Select-Object Name, CurrentReading | ConvertTo-Json',\n  ),\n\n  /// Get SMART disk health information on Windows using Storage cmdlets\n  ///\n  /// Uses Windows PowerShell storage management cmdlets:\n  /// - Get-PhysicalDisk retrieves all physical storage devices\n  /// - Get-StorageReliabilityCounter gets SMART health data via pipeline\n  /// - Selects key health metrics: DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours\n  /// - Outputs results in JSON format for consistent parsing\n  /// - Works with NVMe, SATA, and other storage interfaces supported by Windows\n  /// - May require elevated privileges on some systems\n  diskSmart(\n    'Get-PhysicalDisk | Get-StorageReliabilityCounter | '\n    'Select-Object DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours | '\n    'ConvertTo-Json',\n  ),\n  cpuBrand('(Get-WmiObject -Class Win32_Processor).Name');\n\n  @override\n  final String cmd;\n\n  const WindowsStatusCmdType(this.cmd);\n\n  @override\n  String get separator => ScriptConstants.getCmdSeparator(name);\n\n  @override\n  String get divider => ScriptConstants.getWindowsCmdDivider(name);\n\n  @override\n  CmdTypeSys get sysType => CmdTypeSys.windows;\n}\n\n/// Extensions for StatusCmdType\nextension StatusCmdTypeX on StatusCmdType {\n  String get i18n => switch (this) {\n    StatusCmdType.sys => l10n.system,\n    StatusCmdType.host => libL10n.host,\n    StatusCmdType.uptime => libL10n.uptime,\n    StatusCmdType.battery => libL10n.battery,\n    StatusCmdType.sensors => libL10n.sensors,\n    StatusCmdType.disk => libL10n.disk,\n    final val => val.name,\n  };\n}\n\n/// Extension for CommandType to find content in parsed map\nextension CommandTypeX on ShellCmdType {\n  /// Find the command output from the parsed script output map\n  String findInMap(Map<String, String> parsedOutput) {\n    return parsedOutput[name] ?? '';\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/scripts/script_builders.dart",
    "content": "import 'package:server_box/data/model/app/scripts/cmd_types.dart';\nimport 'package:server_box/data/model/app/scripts/script_consts.dart';\nimport 'package:server_box/data/model/app/scripts/shell_func.dart';\n\n/// Abstract base class for platform-specific script builders\nsealed class ScriptBuilder {\n  const ScriptBuilder();\n\n  /// Generate a complete script for all shell functions\n  String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]);\n\n  /// Get the script file name for this platform\n  String get scriptFileName;\n\n  /// Get the command to install the script\n  String getInstallCommand(String scriptDir, String scriptPath);\n\n  /// Get the execution command for a specific function\n  String getExecCommand(String scriptPath, ShellFunc func);\n\n  /// Get custom commands string for this platform\n  String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds);\n\n  /// Get the script header for this platform\n  String get scriptHeader;\n}\n\n/// Windows PowerShell script builder\nclass WindowsScriptBuilder extends ScriptBuilder {\n  const WindowsScriptBuilder();\n\n  @override\n  String get scriptFileName => ScriptConstants.scriptFileWindows;\n\n  @override\n  String get scriptHeader => ScriptConstants.windowsScriptHeader;\n\n  @override\n  String getInstallCommand(String scriptDir, String scriptPath) {\n    return 'New-Item -ItemType Directory -Force -Path \\'$scriptDir\\' | Out-Null; '\n        '\\$content = [System.Console]::In.ReadToEnd(); '\n        'Set-Content -Path \\'$scriptPath\\' -Value \\$content -Encoding UTF8';\n  }\n\n  @override\n  String getExecCommand(String scriptPath, ShellFunc func) {\n    return 'powershell -ExecutionPolicy Bypass -File \"$scriptPath\" -${func.flag}';\n  }\n\n  @override\n  String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) {\n    if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {\n      final sb = StringBuffer();\n      for (final e in customCmds.entries) {\n        final cmdDivider = ScriptConstants.getCustomCmdSeparator(e.key);\n        sb.writeln('    Write-Host \"$cmdDivider\"');\n        sb.writeln('    ${e.value}');\n      }\n      return '\\n$sb';\n    }\n    return '';\n  }\n\n  @override\n  String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]) {\n    final sb = StringBuffer();\n    sb.write(scriptHeader);\n\n    // Write each function\n    for (final func in ShellFunc.values) {\n      final customCmdsStr = getCustomCmdsString(func, customCmds);\n\n      sb.write('''\nfunction ${func.name} {\n    ${_getWindowsCommand(func, disabledCmdTypes).split('\\n').map((e) => e.isEmpty ? '' : '    $e').join('\\n')}$customCmdsStr\n}\n\n''');\n    }\n\n    // Write switch case\n    sb.write('''\nswitch (\\$args[0]) {\n''');\n    for (final func in ShellFunc.values) {\n      sb.write('''\n    \"-${func.flag}\" { ${func.name} }\n''');\n    }\n    sb.write('''\n    default { Write-Host \"Invalid argument \\$(\\$args[0])\" }\n}\n''');\n    return sb.toString();\n  }\n\n  /// Get Windows-specific command for a shell function\n  String _getWindowsCommand(ShellFunc func, [List<String>? disabledCmdTypes]) => switch (func) {\n    ShellFunc.status => _getWindowsStatusCommand(disabledCmdTypes: disabledCmdTypes ?? []),\n    ShellFunc.process => 'Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet | ConvertTo-Json',\n    ShellFunc.shutdown => 'Stop-Computer -Force',\n    ShellFunc.reboot => 'Restart-Computer -Force',\n    ShellFunc.suspend =>\n      'Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState(\\'Suspend\\', \\$false, \\$false)',\n  };\n\n  /// Get Windows status command with command-specific separators\n  String _getWindowsStatusCommand({required List<String> disabledCmdTypes}) {\n    final cmdTypes = WindowsStatusCmdType.values.where((e) => !disabledCmdTypes.contains(e.displayName));\n    return cmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight(); // Remove trailing divider\n  }\n}\n\n/// Unix shell script builder\nclass UnixScriptBuilder extends ScriptBuilder {\n  const UnixScriptBuilder();\n\n  @override\n  String get scriptFileName => ScriptConstants.scriptFile;\n\n  @override\n  String get scriptHeader => ScriptConstants.unixScriptHeader;\n\n  @override\n  String getInstallCommand(String scriptDir, String scriptPath) {\n    return '''\nmkdir -p $scriptDir\ncat > $scriptPath\nchmod 755 $scriptPath\n''';\n  }\n\n  @override\n  String getExecCommand(String scriptPath, ShellFunc func) {\n    return 'sh $scriptPath -${func.flag}';\n  }\n\n  @override\n  String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) {\n    if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {\n      final sb = StringBuffer();\n      for (final e in customCmds.entries) {\n        final cmdDivider = ScriptConstants.getCustomCmdSeparator(e.key);\n        sb.writeln('echo \"$cmdDivider\"');\n        sb.writeln(e.value);\n      }\n      return '\\n$sb';\n    }\n    return '';\n  }\n\n  @override\n  String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]) {\n    final sb = StringBuffer();\n    sb.write(scriptHeader);\n    // Write each function\n    for (final func in ShellFunc.values) {\n      final customCmdsStr = getCustomCmdsString(func, customCmds);\n      sb.write('''\n${func.name}() {\n${_getUnixCommand(func, disabledCmdTypes).split('\\n').map((e) => '\\t$e').join('\\n')}\n$customCmdsStr\n}\n\n''');\n    }\n\n    // Write switch case\n    sb.write('case \\$1 in\\n');\n    for (final func in ShellFunc.values) {\n      sb.write('''\n  '-${func.flag}')\n    ${func.name}\n    ;;\n''');\n    }\n    sb.write('''\n  *)\n    echo \"Invalid argument \\$1\"\n    ;;\nesac''');\n    return sb.toString();\n  }\n\n  /// Get Unix-specific command for a shell function\n  String _getUnixCommand(ShellFunc func, [List<String>? disabledCmdTypes]) {\n    return switch (func) {\n      ShellFunc.status => _getUnixStatusCommand(disabledCmdTypes: disabledCmdTypes ?? []),\n      ShellFunc.process => _getUnixProcessCommand(),\n      ShellFunc.shutdown => _getUnixShutdownCommand(),\n      ShellFunc.reboot => _getUnixRebootCommand(),\n      ShellFunc.suspend => _getUnixSuspendCommand(),\n    };\n  }\n\n  /// Get Unix status command with OS detection\n  String _getUnixStatusCommand({required List<String> disabledCmdTypes}) {\n    // Generate command lists with command-specific separators, filtering disabled commands\n    final filteredLinuxCmdTypes = StatusCmdType.values.where(\n      (e) => !disabledCmdTypes.contains(e.displayName),\n    );\n    final linuxCommands = filteredLinuxCmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight();\n\n    final filteredBsdCmdTypes = BSDStatusCmdType.values.where(\n      (e) => !disabledCmdTypes.contains(e.displayName),\n    );\n    final bsdCommands = filteredBsdCmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight();\n\n    return '''\nif [ \"\\$macSign\" = \"\" ] && [ \"\\$bsdSign\" = \"\" ]; then\n\\t$linuxCommands\nelse\n\\t$bsdCommands\nfi''';\n  }\n\n  /// Get Unix process command with busybox detection\n  String _getUnixProcessCommand() {\n    return '''\nif [ \"\\$macSign\" = \"\" ] && [ \"\\$bsdSign\" = \"\" ]; then\n\\tif [ \"\\$isBusybox\" != \"\" ]; then\n\\t\\tps w\n\\telse\n\\t\\tps -aux\n\\tfi\nelse\n\\tps -ax\nfi''';\n  }\n\n  /// Get Unix shutdown command with privilege detection\n  String _getUnixShutdownCommand() {\n    return '''\nif [ \"\\$userId\" = \"0\" ]; then\n\\tshutdown -h now\nelse\n\\tsudo -S shutdown -h now\nfi''';\n  }\n\n  /// Get Unix reboot command with privilege detection\n  String _getUnixRebootCommand() {\n    return '''\nif [ \"\\$userId\" = \"0\" ]; then\n\\treboot\nelse\n\\tsudo -S reboot\nfi''';\n  }\n\n  /// Get Unix suspend command with privilege detection\n  String _getUnixSuspendCommand() {\n    return '''\nif [ \"\\$userId\" = \"0\" ]; then\n\\tsystemctl suspend\nelse\n\\tsudo -S systemctl suspend\nfi''';\n  }\n}\n\n/// Factory class to get appropriate script builder for platform\nclass ScriptBuilderFactory {\n  const ScriptBuilderFactory._();\n\n  /// Get the appropriate script builder based on platform\n  static ScriptBuilder getBuilder(bool isWindows) {\n    return isWindows ? const WindowsScriptBuilder() : const UnixScriptBuilder();\n  }\n\n  /// Get all available builders (useful for testing)\n  static List<ScriptBuilder> getAllBuilders() {\n    return const [WindowsScriptBuilder(), UnixScriptBuilder()];\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/scripts/script_consts.dart",
    "content": "import 'package:server_box/data/res/build_data.dart';\n\n/// Constants used throughout the script system\nclass ScriptConstants {\n  const ScriptConstants._();\n\n  // Script file names\n  static const String scriptFile = 'srvboxm_v${BuildData.script}.sh';\n  static const String scriptFileWindows = 'srvboxm_v${BuildData.script}.ps1';\n\n  // Script directories\n  static const String scriptDirHome = '~/.config/server_box';\n  static const String scriptDirTmp = '/tmp/server_box';\n  static const String scriptDirHomeWindows = '%USERPROFILE%/.config/server_box';\n  static const String scriptDirTmpWindows = '%TEMP%/server_box';\n\n  // Command separators and dividers\n  static const String separator = 'SrvBoxSep';\n\n  /// Custom command separator\n  static const String customCmdSep = 'SrvBoxCusCmdSep';\n\n  /// Generate command-specific separator\n  static String getCmdSeparator(String cmdName) => '$separator.$cmdName';\n\n  /// Generate command-specific divider for custom commands\n  static String getCustomCmdSeparator(String cmdName) => '$customCmdSep.$cmdName';\n\n  /// Generate command-specific divider\n  static String getCmdDivider(String cmdName) => '\\necho ${getCmdSeparator(cmdName)}\\n\\t';\n\n  /// Generate command-specific divider for Windows PowerShell\n  static String getWindowsCmdDivider(String cmdName) => '\\n    Write-Host \"${getCmdSeparator(cmdName)}\"\\n    ';\n\n  /// Parse script output into command-specific map\n  static Map<String, String> parseScriptOutput(String raw) {\n    final result = <String, String>{};\n\n    if (raw.isEmpty) return result;\n\n    // Parse line by line to properly handle command-specific separators\n    final lines = raw.split('\\n');\n    String? currentCmd;\n    final buffer = StringBuffer();\n\n    for (final line in lines) {\n      if (line.startsWith('$separator.')) {\n        // Save previous command content\n        if (currentCmd != null) {\n          result[currentCmd] = buffer.toString().trim();\n          buffer.clear();\n        }\n        // Start new command\n        currentCmd = line.substring('$separator.'.length);\n      } else if (line.startsWith('$customCmdSep.')) {\n        // Save previous command content\n        if (currentCmd != null) {\n          result[currentCmd] = buffer.toString().trim();\n          buffer.clear();\n        }\n        // Start new custom command\n        currentCmd = line.substring('$customCmdSep.'.length);\n      } else if (currentCmd != null) {\n        buffer.writeln(line);\n      }\n    }\n\n    // Don't forget the last command\n    if (currentCmd != null) {\n      result[currentCmd] = buffer.toString().trim();\n    }\n\n    return result;\n  }\n\n  // Path separators\n  static const String unixPathSeparator = '/';\n  static const String windowsPathSeparator = '\\\\';\n\n  // Script headers\n  static const String unixScriptHeader =\n      '''\n#!/bin/sh\n# Script for ServerBox app v1.0.${BuildData.build}\n# DO NOT delete this file while app is running\n\nexport LANG=en_US.UTF-8\n\n# If macSign & bsdSign are both empty, then it's linux\nmacSign=\\$(uname -a 2>&1 | grep \"Darwin\")\nbsdSign=\\$(uname -a 2>&1 | grep \"BSD\")\n\n# Link /bin/sh to busybox?\nisBusybox=\\$(ls -l /bin/sh | grep \"busybox\")\n\nuserId=\\$(id -u)\n\nexec 2>/dev/null\n\n''';\n\n  static const String windowsScriptHeader =\n      '''\n# PowerShell script for ServerBox app v1.0.${BuildData.build}\n# DO NOT delete this file while app is running\n\n\\$ErrorActionPreference = \"SilentlyContinue\"\n[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n\n''';\n}\n\n/// Script path configuration and management\nclass ScriptPaths {\n  ScriptPaths._();\n\n  static final Map<String, String> _scriptDirMap = <String, String>{};\n\n  /// Get the script directory for the given [id].\n  ///\n  /// Default is [ScriptConstants.scriptDirTmp]/[ScriptConstants.scriptFile],\n  /// if this path is not accessible, it will be changed to\n  /// [ScriptConstants.scriptDirHome]/[ScriptConstants.scriptFile].\n  static String getScriptDir(String id, {bool isWindows = false}) {\n    final defaultTmpDir = isWindows ? ScriptConstants.scriptDirTmpWindows : ScriptConstants.scriptDirTmp;\n    _scriptDirMap[id] ??= defaultTmpDir;\n    return _scriptDirMap[id]!;\n  }\n\n  /// Switch between tmp and home directories for script storage\n  static String switchScriptDir(String id, {bool isWindows = false}) {\n    return switch (_scriptDirMap[id]) {\n      ScriptConstants.scriptDirTmp => _scriptDirMap[id] = ScriptConstants.scriptDirHome,\n      ScriptConstants.scriptDirTmpWindows => _scriptDirMap[id] = ScriptConstants.scriptDirHomeWindows,\n      ScriptConstants.scriptDirHome => _scriptDirMap[id] = ScriptConstants.scriptDirTmp,\n      ScriptConstants.scriptDirHomeWindows => _scriptDirMap[id] = ScriptConstants.scriptDirTmpWindows,\n      _ =>\n        _scriptDirMap[id] = isWindows ? ScriptConstants.scriptDirHomeWindows : ScriptConstants.scriptDirHome,\n    };\n  }\n\n  /// Get the full script path for the given [id]\n  static String getScriptPath(String id, {bool isWindows = false}) {\n    final dir = getScriptDir(id, isWindows: isWindows);\n    final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile;\n    final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;\n    return '$dir$separator$fileName';\n  }\n\n  /// Clear cached script directories (useful for testing)\n  static void clearCache() {\n    _scriptDirMap.clear();\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/scripts/shell_func.dart",
    "content": "import 'package:server_box/data/model/app/scripts/script_builders.dart';\nimport 'package:server_box/data/model/app/scripts/script_consts.dart';\nimport 'package:server_box/data/model/server/system.dart';\n\n/// Shell functions available in the ServerBox application\nenum ShellFunc {\n  status('SbStatus'),\n  process('SbProcess'),\n  shutdown('SbShutdown'),\n  reboot('SbReboot'),\n  suspend('SbSuspend');\n\n  /// The function name used in scripts\n  final String name;\n\n  const ShellFunc(this.name);\n\n  /// Get the command line flag for this function\n  String get flag => switch (this) {\n    ShellFunc.process => 'p',\n    ShellFunc.shutdown => 'sd',\n    ShellFunc.reboot => 'r',\n    ShellFunc.suspend => 'sp',\n    ShellFunc.status => 's',\n  };\n\n  /// Execute this shell function on the specified server\n  String exec(String id, {SystemType? systemType, required String? customDir}) {\n    final scriptPath = ShellFuncManager.getScriptPath(id, systemType: systemType, customDir: customDir);\n    final isWindows = systemType == SystemType.windows;\n    final builder = ScriptBuilderFactory.getBuilder(isWindows);\n\n    return builder.getExecCommand(scriptPath, this);\n  }\n}\n\n/// Manager class for shell function operations\nclass ShellFuncManager {\n  const ShellFuncManager._();\n\n  /// Normalize a directory path to ensure it doesn't end with trailing separators\n  static String _normalizeDir(String dir, bool isWindows) {\n    final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;\n\n    // Remove all trailing separators\n    final pattern = RegExp('${RegExp.escape(separator)}+\\$');\n    return dir.replaceAll(pattern, '');\n  }\n\n  /// Get the script directory for the given [id].\n  ///\n  /// Checks for custom script directory first, then falls back to default.\n  static String getScriptDir(String id, {SystemType? systemType, required String? customDir}) {\n    final isWindows = systemType == SystemType.windows;\n\n    if (customDir != null) return _normalizeDir(customDir, isWindows);\n    return ScriptPaths.getScriptDir(id, isWindows: isWindows);\n  }\n\n  /// Switch between tmp and home directories for script storage\n  static void switchScriptDir(String id, {SystemType? systemType}) {\n    final isWindows = systemType == SystemType.windows;\n    ScriptPaths.switchScriptDir(id, isWindows: isWindows);\n  }\n\n  /// Get the full script path for the given [id]\n  static String getScriptPath(String id, {SystemType? systemType, required String? customDir}) {\n    if (customDir != null) {\n      final isWindows = systemType == SystemType.windows;\n      final normalizedDir = _normalizeDir(customDir, isWindows);\n      final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile;\n      final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;\n      return '$normalizedDir$separator$fileName';\n    }\n\n    final isWindows = systemType == SystemType.windows;\n    return ScriptPaths.getScriptPath(id, isWindows: isWindows);\n  }\n\n  /// Get the installation shell command for the script\n  static String getInstallShellCmd(String id, {SystemType? systemType, required String? customDir}) {\n    final scriptDir = getScriptDir(id, systemType: systemType, customDir: customDir);\n    final isWindows = systemType == SystemType.windows;\n    final normalizedDir = _normalizeDir(scriptDir, isWindows);\n    final builder = ScriptBuilderFactory.getBuilder(isWindows);\n    final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;\n    final scriptPath = '$normalizedDir$separator${builder.scriptFileName}';\n\n    return builder.getInstallCommand(normalizedDir, scriptPath);\n  }\n\n  /// Generate complete script based on system type\n  static String allScript(\n    Map<String, String>? customCmds, {\n    SystemType? systemType,\n    List<String>? disabledCmdTypes,\n  }) {\n    final isWindows = systemType == SystemType.windows;\n    final builder = ScriptBuilderFactory.getBuilder(isWindows);\n\n    return builder.buildScript(customCmds, disabledCmdTypes);\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/server_detail_card.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/res/store.dart';\n\nenum ServerDetailCards {\n  about(Icons.info),\n  cpu(Icons.memory),\n  mem(Bootstrap.memory),\n  swap(Icons.swap_horiz),\n  gpu(Bootstrap.gpu_card),\n  disk(Bootstrap.device_hdd_fill),\n  smart(Icons.health_and_safety, sinceBuild: 1174),\n  net(ZondIcons.network),\n  sensor(MingCute.dashboard_4_line),\n  temp(FontAwesome.temperature_empty_solid),\n  battery(Icons.battery_full),\n  pve(BoxIcons.bxs_dashboard, sinceBuild: 818),\n  custom(Icons.code, sinceBuild: 825);\n\n  final int? sinceBuild;\n\n  final IconData icon;\n\n  const ServerDetailCards(this.icon, {this.sinceBuild});\n\n  static ServerDetailCards? fromName(String str) =>\n      ServerDetailCards.values.firstWhereOrNull((e) => e.name == str);\n\n  static final names = values.map((e) => e.name).toList();\n\n  String get toStr => switch (this) {\n    about => libL10n.about,\n    cpu => 'CPU',\n    mem => 'RAM',\n    swap => 'Swap',\n    gpu => 'GPU',\n    disk => libL10n.disk,\n    smart => l10n.diskHealth,\n    net => libL10n.net,\n    sensor => libL10n.sensors,\n    temp => libL10n.temperature,\n    battery => libL10n.battery,\n    pve => 'PVE',\n    custom => libL10n.cmd,\n  };\n\n  /// If:\n  /// Version 1 => user set [about], default is [about, cpu]\n  /// Version 2 => default is [about, cpu, mem] => auto add [mem] to user's setting\n  static void autoAddNewCards(int cur) {\n    if (cur >= pve.sinceBuild!) {\n      final prop = Stores.setting.detailCardOrder;\n      final list = prop.fetch();\n      if (!list.contains(pve.name)) {\n        list.add(pve.name);\n        prop.put(list);\n      }\n    }\n\n    if (cur >= custom.sinceBuild!) {\n      final prop = Stores.setting.detailCardOrder;\n      final list = prop.fetch();\n      if (!list.contains(custom.name)) {\n        list.add(custom.name);\n        prop.put(list);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/tab.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:hive_ce_flutter/adapters.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/view/page/server/tab/tab.dart';\n// import 'package:server_box/view/page/setting/entry.dart';\nimport 'package:server_box/view/page/snippet/list.dart';\nimport 'package:server_box/view/page/ssh/tab.dart';\nimport 'package:server_box/view/page/storage/local.dart';\n\npart 'tab.g.dart';\n\n@HiveType(typeId: 103)\nenum AppTab {\n  @HiveField(0)\n  server,\n  @HiveField(1)\n  ssh,\n  @HiveField(2)\n  file,\n  @HiveField(3)\n  snippet\n  //settings,\n  ;\n\n  Widget get page {\n    return switch (this) {\n      server => const ServerPage(),\n      //settings => const SettingsPage(),\n      ssh => const SSHTabPage(),\n      file => const LocalFilePage(),\n      snippet => const SnippetListPage(),\n    };\n  }\n\n  NavigationDestination get navDestination {\n    return switch (this) {\n      server => NavigationDestination(\n        icon: const Icon(BoxIcons.bx_server),\n        label: libL10n.server,\n        selectedIcon: const Icon(BoxIcons.bxs_server),\n      ),\n      // settings => NavigationDestination(\n      //     icon: const Icon(Icons.settings),\n      //     label: libL10n.setting,\n      //     selectedIcon: const Icon(Icons.settings),\n      //   ),\n      ssh => const NavigationDestination(\n        icon: Icon(Icons.terminal_outlined),\n        label: 'SSH',\n        selectedIcon: Icon(Icons.terminal),\n      ),\n      snippet => NavigationDestination(\n        icon: const Icon(Icons.code),\n        label: libL10n.snippet,\n        selectedIcon: const Icon(Icons.code),\n      ),\n      file => NavigationDestination(\n        icon: const Icon(Icons.folder_open),\n        label: libL10n.file,\n        selectedIcon: const Icon(Icons.folder),\n      ),\n    };\n  }\n\n  NavigationRailDestination get navRailDestination {\n    return switch (this) {\n      server => NavigationRailDestination(\n        icon: const Icon(BoxIcons.bx_server),\n        label: Text(libL10n.server),\n        selectedIcon: const Icon(BoxIcons.bxs_server),\n      ),\n      // settings => NavigationRailDestination(\n      //     icon: const Icon(Icons.settings),\n      //     label: libL10n.setting,\n      //     selectedIcon: const Icon(Icons.settings),\n      //   ),\n      ssh => const NavigationRailDestination(\n        icon: Icon(Icons.terminal_outlined),\n        label: Text('SSH'),\n        selectedIcon: Icon(Icons.terminal),\n      ),\n      snippet => NavigationRailDestination(\n        icon: const Icon(Icons.code),\n        label: Text(libL10n.snippet),\n        selectedIcon: const Icon(Icons.code),\n      ),\n      file => NavigationRailDestination(\n        icon: const Icon(Icons.folder_open),\n        label: Text(libL10n.file),\n        selectedIcon: const Icon(Icons.folder),\n      ),\n    };\n  }\n\n  static List<NavigationDestination> get navDestinations {\n    return AppTab.values.map((e) => e.navDestination).toList();\n  }\n\n  static List<NavigationRailDestination> get navRailDestinations {\n    return AppTab.values.map((e) => e.navRailDestination).toList();\n  }\n\n\n\n  /// Helper function to parse AppTab list from stored object\n  static List<AppTab> parseAppTabsFromObj(dynamic val) {\n    if (val is List) {\n      final tabs = <AppTab>[];\n      for (final e in val) {\n        final tab = _parseAppTabFromElement(e);\n        if (tab != null) {\n          tabs.add(tab);\n        }\n      }\n      if (tabs.isNotEmpty) return tabs;\n    }\n    return AppTab.values;\n  }\n\n  /// Helper function to parse a single AppTab from various element types\n  static AppTab? _parseAppTabFromElement(dynamic e) {\n    if (e is AppTab) {\n      return e;\n    } else if (e is String) {\n      return AppTab.values.firstWhereOrNull((t) => t.name == e);\n    } else if (e is int) {\n      if (e >= 0 && e < AppTab.values.length) {\n        return AppTab.values[e];\n      }\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/app/tab.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'tab.dart';\n\n// **************************************************************************\n// TypeAdapterGenerator\n// **************************************************************************\n\nclass AppTabAdapter extends TypeAdapter<AppTab> {\n  @override\n  final typeId = 103;\n\n  @override\n  AppTab read(BinaryReader reader) {\n    switch (reader.readByte()) {\n      case 0:\n        return AppTab.server;\n      case 1:\n        return AppTab.ssh;\n      case 2:\n        return AppTab.file;\n      case 3:\n        return AppTab.snippet;\n      default:\n        return AppTab.server;\n    }\n  }\n\n  @override\n  void write(BinaryWriter writer, AppTab obj) {\n    switch (obj) {\n      case AppTab.server:\n        writer.writeByte(0);\n      case AppTab.ssh:\n        writer.writeByte(1);\n      case AppTab.file:\n        writer.writeByte(2);\n      case AppTab.snippet:\n        writer.writeByte(3);\n    }\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppTabAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n"
  },
  {
    "path": "lib/data/model/container/image.dart",
    "content": "import 'dart:convert';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/container/type.dart';\n\nabstract final class ContainerImg {\n  final String? repository = null;\n  final String? tag = null;\n  final String? id = null;\n  String? get sizeMB;\n  int? get containersCount;\n\n  factory ContainerImg.fromRawJson(String s, ContainerType typ) => typ.img(s);\n}\n\nfinal class PodmanImg implements ContainerImg {\n  @override\n  final String? repository;\n  @override\n  final String? tag;\n  @override\n  final String? id;\n  final int? created;\n  final int? size;\n  final int? containers;\n\n  PodmanImg({this.repository, this.tag, this.id, this.created, this.size, this.containers});\n\n  @override\n  String? get sizeMB => size?.bytes2Str;\n\n  @override\n  int? get containersCount => containers;\n\n  factory PodmanImg.fromRawJson(String str) => PodmanImg.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(\n    repository: _asString(json['repository']),\n    tag: _asString(json['tag']),\n    id: _asString(json['Id']),\n    created: _asInt(json['Created']),\n    size: _asInt(json['Size']),\n    containers: _asInt(json['Containers']),\n  );\n\n  Map<String, dynamic> toJson() => {\n    'repository': repository,\n    'tag': tag,\n    'Id': id,\n    'Created': created,\n    'Size': size,\n    'Containers': containers,\n  };\n}\n\nfinal class DockerImg implements ContainerImg {\n  final String containers;\n  final String createdAt;\n  @override\n  final String id;\n  @override\n  final String repository;\n  final String size;\n  @override\n  final String? tag;\n\n  DockerImg({\n    required this.containers,\n    required this.createdAt,\n    required this.id,\n    required this.repository,\n    required this.size,\n    required this.tag,\n  });\n\n  @override\n  String? get sizeMB => size;\n\n  @override\n  int? get containersCount => containers == 'N/A' ? 0 : int.tryParse(containers);\n\n  factory DockerImg.fromRawJson(String str) => DockerImg.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  factory DockerImg.fromJson(Map<String, dynamic> json) {\n    final containers = switch (json['Containers']) {\n      final String a => a,\n      final Object? a => a.toString(),\n    };\n    final repo = switch (json['Repository'] ?? json['Names']) {\n      final String a => a,\n      final List a => a.firstOrNull.toString(),\n      final Object? a => a.toString(),\n    };\n    final size = switch (json['Size']) {\n      final String a => a,\n      final int a => a.bytes2Str,\n      final Object? a => a.toString(),\n    };\n    return DockerImg(\n      containers: containers,\n      createdAt: json['CreatedAt'],\n      id: json['ID'] ?? json['Id'] ?? '',\n      repository: repo,\n      size: size,\n      tag: json['Tag'],\n    );\n  }\n\n  Map<String, dynamic> toJson() => {\n    'Containers': containers,\n    'CreatedAt': createdAt,\n    'ID': id,\n    'Repository': repository,\n    'Size': size,\n    'Tag': tag,\n  };\n}\n\nString? _asString(dynamic val) {\n  if (val == null) return null;\n  if (val is String) return val;\n  return val.toString();\n}\n\nint? _asInt(dynamic val) {\n  if (val == null) return null;\n  if (val is int) return val;\n  if (val is double) return val.toInt();\n  return int.tryParse(val.toString());\n}\n"
  },
  {
    "path": "lib/data/model/container/ps.dart",
    "content": "import 'dart:convert';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/container/status.dart';\nimport 'package:server_box/data/model/container/type.dart';\nimport 'package:server_box/data/res/misc.dart';\n\nsealed class ContainerPs {\n  final String? id = null;\n  final String? image = null;\n  String? get name;\n  String? get cmd;\n  ContainerStatus get status;\n\n  String? cpu;\n  String? mem;\n  String? net;\n  String? disk;\n\n  factory ContainerPs.fromRaw(String s, ContainerType typ) => typ.ps(s);\n\n  void parseStats(String s, [String? version]);\n}\n\nfinal class PodmanPs implements ContainerPs {\n  final List<String>? command;\n  final DateTime? created;\n  final bool? exited;\n  @override\n  final String? id;\n  @override\n  final String? image;\n  final List<String>? names;\n  final int? startedAt;\n\n  @override\n  String? cpu;\n  @override\n  String? mem;\n  @override\n  String? net;\n  @override\n  String? disk;\n\n  PodmanPs({this.command, this.created, this.exited, this.id, this.image, this.names, this.startedAt});\n\n  @override\n  String? get name => names?.firstOrNull;\n\n  @override\n  String? get cmd => command?.firstOrNull;\n\n  @override\n  ContainerStatus get status => ContainerStatus.fromPodmanExited(exited);\n\n  @override\n  void parseStats(String s, [String? version]) {\n    final stats = json.decode(s);\n    final cpuD = (stats['CPU'] as double? ?? 0).toStringAsFixed(1);\n    final cpuAvgD = (stats['AvgCPU'] as double? ?? 0).toStringAsFixed(1);\n    cpu = '$cpuD% / ${libL10n.pingAvg} $cpuAvgD%';\n    final memLimit = (stats['MemLimit'] as int? ?? 0).bytes2Str;\n    final memUsage = (stats['MemUsage'] as int? ?? 0).bytes2Str;\n    mem = '$memUsage / $memLimit';\n\n    int netIn = 0;\n    int netOut = 0;\n    final majorVersion = version?.split('.').firstOrNull;\n    final majorVersionNum = majorVersion != null ? int.tryParse(majorVersion) : null;\n\n    // Podman 4.x and earlier use top-level NetInput/NetOutput fields.\n    // Podman 5.x changed network backend (Netavark) and uses nested\n    // Network.{iface}.RxBytes/TxBytes structure instead.\n    if (majorVersionNum == null || majorVersionNum <= 4) {\n      netIn = stats['NetInput'] as int? ?? 0;\n      netOut = stats['NetOutput'] as int? ?? 0;\n    } else if (majorVersionNum >= 5) {\n      final network = stats['Network'] as Map<String, dynamic>?;\n      if (network != null) {\n        for (final interface in network.values) {\n          netIn += interface['RxBytes'] as int? ?? 0;\n          netOut += interface['TxBytes'] as int? ?? 0;\n        }\n      }\n    }\n    net = '↓ ${netIn.bytes2Str} / ↑ ${netOut.bytes2Str}';\n\n    final diskIn = (stats['BlockInput'] as int? ?? 0).bytes2Str;\n    final diskOut = (stats['BlockOutput'] as int? ?? 0).bytes2Str;\n    disk = '${l10n.read} $diskIn / ${l10n.write} $diskOut';\n  }\n\n  factory PodmanPs.fromRawJson(String str) => PodmanPs.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(\n    command: json['Command'] == null ? [] : List<String>.from(json['Command']!.map((x) => x)),\n    created: json['Created'] == null ? null : DateTime.parse(json['Created']),\n    exited: json['Exited'],\n    id: json['Id'],\n    image: json['Image'],\n    names: json['Names'] == null ? [] : List<String>.from(json['Names']!.map((x) => x)),\n    startedAt: json['StartedAt'],\n  );\n\n  Map<String, dynamic> toJson() => {\n    'Command': command == null ? [] : List<dynamic>.from(command!.map((x) => x)),\n    'Created': created?.toIso8601String(),\n    'Exited': exited,\n    'Id': id,\n    'Image': image,\n    'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),\n    'StartedAt': startedAt,\n  };\n}\n\nfinal class DockerPs implements ContainerPs {\n  @override\n  final String? id;\n  @override\n  final String? image;\n  final String? names;\n  final String? state;\n\n  @override\n  String? cpu;\n  @override\n  String? mem;\n  @override\n  String? net;\n  @override\n  String? disk;\n\n  DockerPs({this.id, this.image, this.names, this.state});\n\n  @override\n  String? get name => names;\n\n  @override\n  String? get cmd => null;\n\n  @override\n  ContainerStatus get status => ContainerStatus.fromDockerState(state);\n\n  @override\n  void parseStats(String s, [String? version]) {\n    final stats = json.decode(s);\n    cpu = stats['CPUPerc'];\n    mem = stats['MemUsage'];\n\n    final netIO = stats['NetIO'] as String? ?? '0B / 0B';\n    final netParts = netIO.split(' / ');\n    net = '↓ ${netParts.firstOrNull ?? '0B'} / ↑ ${netParts.length > 1 ? netParts[1] : '0B'}';\n\n    final blockIO = stats['BlockIO'] as String? ?? '0B / 0B';\n    final blockParts = blockIO.split(' / ');\n    disk = '${l10n.read} ${blockParts.firstOrNull ?? '0B'} / ${l10n.write} ${blockParts.length > 1 ? blockParts[1] : '0B'}';\n  }\n\n  /// CONTAINER ID                   NAMES                          IMAGE                          STATUS\n  /// a049d689e7a1                   aria2-pro                      p3terx/aria2-pro               Up 3 weeks\n  factory DockerPs.parse(String raw) {\n    final parts = raw.split(Miscs.multiBlankreg);\n    return DockerPs(id: parts[0], state: parts[1], names: parts[2], image: parts[3].trim());\n  }\n}\n"
  },
  {
    "path": "lib/data/model/container/status.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\n\n/// Represents the various states a container can be in.\n/// Supports both Docker and Podman container status parsing.\nenum ContainerStatus {\n  running,\n  exited,\n  created,\n  paused,\n  restarting,\n  removing,\n  dead,\n  unknown;\n\n  /// Check if the container is actively running\n  bool get isRunning => this == ContainerStatus.running;\n\n  /// Check if the container can be started\n  bool get canStart =>\n      this == ContainerStatus.exited ||\n      this == ContainerStatus.created ||\n      this == ContainerStatus.dead;\n\n  /// Check if the container can be stopped\n  bool get canStop =>\n      this == ContainerStatus.running || this == ContainerStatus.paused;\n\n  /// Check if the container can be restarted\n  bool get canRestart =>\n      this != ContainerStatus.removing && this != ContainerStatus.unknown;\n\n  /// Parse Docker container status string to ContainerStatus\n  static ContainerStatus fromDockerState(String? state) {\n    if (state == null || state.isEmpty) return ContainerStatus.unknown;\n\n    final lowerState = state.toLowerCase();\n\n    if (lowerState.startsWith('up')) return ContainerStatus.running;\n    if (lowerState.contains('exited')) return ContainerStatus.exited;\n    if (lowerState.contains('created')) return ContainerStatus.created;\n    if (lowerState.contains('paused')) return ContainerStatus.paused;\n    if (lowerState.contains('restarting')) return ContainerStatus.restarting;\n    if (lowerState.contains('removing')) return ContainerStatus.removing;\n    if (lowerState.contains('dead')) return ContainerStatus.dead;\n\n    return ContainerStatus.unknown;\n  }\n\n  /// Parse Podman container status from exited boolean\n  static ContainerStatus fromPodmanExited(bool? exited) {\n    if (exited == true) return ContainerStatus.exited;\n    if (exited == false) return ContainerStatus.running;\n    return ContainerStatus.unknown;\n  }\n\n  /// Get display string for the status\n  String get displayName {\n    return switch (this) {\n      ContainerStatus.running => libL10n.running,\n      ContainerStatus.exited => libL10n.exit,\n      ContainerStatus.created => 'Created',\n      ContainerStatus.paused => 'Paused',\n      ContainerStatus.restarting => 'Restarting',\n      ContainerStatus.removing => 'Removing',\n      ContainerStatus.dead => 'Dead',\n      ContainerStatus.unknown => libL10n.unknown,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/data/model/container/type.dart",
    "content": "import 'package:server_box/data/model/container/image.dart';\nimport 'package:server_box/data/model/container/ps.dart';\n\nenum ContainerType {\n  docker,\n  podman;\n\n  ContainerPs Function(String str) get ps => switch (this) {\n    ContainerType.docker => DockerPs.parse,\n    ContainerType.podman => PodmanPs.fromRawJson,\n  };\n\n  ContainerImg Function(String str) get img => switch (this) {\n    ContainerType.docker => DockerImg.fromRawJson,\n    ContainerType.podman => PodmanImg.fromRawJson,\n  };\n}\n"
  },
  {
    "path": "lib/data/model/pkg/manager.dart",
    "content": "import 'package:server_box/data/model/server/dist.dart';\n\nenum PkgManager {\n  apt,\n  yum,\n  zypper,\n  pacman,\n  opkg,\n  apk;\n\n  String? get listUpdate {\n    switch (this) {\n      case PkgManager.yum:\n        return 'yum check-update';\n      case PkgManager.apt:\n        return 'apt list --upgradeable';\n      case PkgManager.zypper:\n        return 'zypper lu';\n      case PkgManager.pacman:\n        return 'pacman -Qu';\n      case PkgManager.opkg:\n        return 'opkg list-upgradable';\n      case PkgManager.apk:\n        return 'apk list --upgradable';\n    }\n  }\n\n  String? get update {\n    switch (this) {\n      case PkgManager.apt:\n        return 'apt update';\n      case PkgManager.pacman:\n        return 'pacman -Sy';\n      case PkgManager.opkg:\n        return 'opkg update';\n      case PkgManager.apk:\n        return 'apk update';\n      default:\n        return null;\n    }\n  }\n\n  String? upgrade(String args) {\n    switch (this) {\n      case PkgManager.yum:\n        return 'yum upgrade -y';\n      case PkgManager.apt:\n        return 'apt upgrade -y';\n      case PkgManager.zypper:\n        return 'zypper up -y';\n      case PkgManager.pacman:\n        return 'pacman -Syu --noconfirm';\n      case PkgManager.opkg:\n        return 'opkg upgrade $args';\n      case PkgManager.apk:\n        return 'apk upgrade';\n    }\n  }\n\n  List<String> updateListRemoveUnused(List<String> list) {\n    switch (this) {\n      case PkgManager.yum:\n        list = list.sublist(2);\n        list.removeWhere((element) => element.isEmpty);\n        final endLine = list.lastIndexWhere((element) => element.contains('Obsoleting Packages'));\n        if (endLine != -1 && list.isNotEmpty) {\n          list = list.sublist(0, endLine);\n        }\n        break;\n      case PkgManager.apt:\n        // avoid other outputs\n        // such as: [Could not chdir to home directory /home/test: No such file or directory, , WARNING: apt does not have a stable CLI interface. Use with caution in scripts., , Listing...]\n        final idx = list.indexWhere((element) => element.contains('[upgradable from:'));\n        if (idx == -1) {\n          return [];\n        }\n        list = list.sublist(idx);\n        list.removeWhere((element) => element.isEmpty);\n        break;\n      case PkgManager.zypper:\n        list = list.sublist(4);\n        break;\n      case PkgManager.pacman:\n      case PkgManager.opkg:\n      case PkgManager.apk:\n        break;\n    }\n    list.removeWhere((element) => element.isEmpty);\n    return list;\n  }\n\n  static PkgManager? fromDist(Dist? dist) {\n    switch (dist) {\n      case Dist.centos:\n      case Dist.rocky:\n      case Dist.fedora:\n        return PkgManager.yum;\n      case Dist.debian:\n      case Dist.ubuntu:\n      case Dist.kali:\n      case Dist.armbian:\n      case Dist.deepin:\n        return PkgManager.apt;\n      case Dist.opensuse:\n        return PkgManager.zypper;\n      case Dist.coreelec:\n      case Dist.wrt:\n        return PkgManager.opkg;\n      case Dist.arch:\n        return PkgManager.pacman;\n      case Dist.alpine:\n        return PkgManager.apk;\n      case null:\n        return null;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/data/model/pkg/upgrade_info.dart",
    "content": "import 'package:server_box/data/model/pkg/manager.dart';\n\nclass UpgradePkgInfo {\n  final PkgManager? _mgr;\n\n  late String package;\n  late String nowVersion;\n  late String newVersion;\n  late String arch;\n\n  UpgradePkgInfo(String raw, this._mgr) {\n    switch (_mgr) {\n      case PkgManager.apt:\n        _parseApt(raw);\n        break;\n      case PkgManager.yum:\n        _parseYum(raw);\n        break;\n      case PkgManager.zypper:\n        _parseZypper(raw);\n        break;\n      case PkgManager.pacman:\n        _parsePacman(raw);\n        break;\n      case PkgManager.opkg:\n        _parseOpkg(raw);\n        break;\n      case PkgManager.apk:\n        _parseApk(raw);\n        break;\n      default:\n        throw Exception('Unsupported pkg type: $_mgr');\n    }\n  }\n\n  void _parseApt(String raw) {\n    final split1 = raw.split('/');\n    package = split1[0];\n    final split2 = split1[1].split(' ');\n    newVersion = split2[1];\n    arch = split2[2];\n    nowVersion = split2[5].replaceFirst(']', '');\n  }\n\n  void _parseYum(String raw) {\n    final result = RegExp(r'\\S+').allMatches(raw);\n    final pkgAndArch = result.elementAt(0).group(0) ?? '.';\n    final split1 = pkgAndArch.split('.');\n    package = split1[0];\n    arch = split1[1];\n    newVersion = result.elementAt(1).group(0) ?? 'Unknown';\n    nowVersion = '';\n  }\n\n  void _parseZypper(String raw) {\n    final cols = raw.split('|');\n    package = cols[2].trim();\n    nowVersion = cols[3].trim();\n    newVersion = cols[4].trim();\n    arch = cols[5].trim();\n  }\n\n  void _parsePacman(String raw) {\n    final parts = raw.split(' ');\n    if (parts.length < 4) throw Exception('Invalid pacman output format');\n    package = parts[0];\n    nowVersion = parts[1];\n    newVersion = parts[3];\n    arch = '';\n  }\n\n  void _parseOpkg(String raw) {\n    final parts = raw.split(' - ');\n    if (parts.length < 3) throw Exception('Invalid opkg output format');\n    package = parts[0];\n    nowVersion = parts[1];\n    newVersion = parts[2];\n    arch = '';\n  }\n\n  // libcrypto3-3.0.8-r4 x86_64 {openssl} (Apache-2.0) [upgradable from: libcrypto3-3.0.8-r3]\n  void _parseApk(String raw) {\n    final parts = raw.split(' ');\n    final len = parts.length;\n    if (len < 2) throw Exception('Invalid apk output format');\n    newVersion = parts[len - 1];\n    nowVersion = parts[0];\n    newVersion = newVersion.substring(0, newVersion.length - 1);\n    package = nowVersion;\n    arch = parts[1];\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/amd.dart",
    "content": "import 'dart:convert';\n\n/// AMD GPU monitoring data structures\n/// Supports both amd-smi and rocm-smi tools\n/// Example JSON output:\n/// [\n///   {\n///     \"name\": \"AMD Radeon RX 7900 XTX\",\n///     \"device_id\": \"0\",\n///     \"temp\": 45,\n///     \"power\": \"120W / 355W\",\n///     \"memory\": {\n///       \"total\": 24576,\n///       \"used\": 1024,\n///       \"unit\": \"MB\",\n///       \"processes\": [\n///         {\n///           \"pid\": 2456,\n///           \"name\": \"firefox\",\n///           \"memory\": 512\n///         }\n///       ]\n///     },\n///     \"utilization\": 75,\n///     \"fan_speed\": 1200,\n///     \"clock_speed\": 2400\n///   }\n/// ]\n\nclass AmdSmi {\n  static List<AmdSmiItem> fromJson(String raw) {\n    try {\n      final jsonData = json.decode(raw);\n      if (jsonData is! List) return [];\n\n      return jsonData\n          .map((gpu) => _parseGpuItem(gpu))\n          .where((item) => item != null)\n          .cast<AmdSmiItem>()\n          .toList();\n    } catch (e) {\n      return [];\n    }\n  }\n\n  static AmdSmiItem? _parseGpuItem(Map<String, dynamic> gpu) {\n    try {\n      final name = gpu['name'] ?? gpu['card_model'] ?? gpu['device_name'] ?? 'Unknown AMD GPU';\n      final deviceId = gpu['device_id']?.toString() ?? gpu['gpu_id']?.toString() ?? '0';\n\n      // Temperature parsing\n      final tempRaw = gpu['temperature'] ?? gpu['temp'] ?? gpu['gpu_temp'];\n      final temp = _parseIntValue(tempRaw);\n\n      // Power parsing\n      final powerDraw = gpu['power_draw'] ?? gpu['current_power'];\n      final powerCap = gpu['power_cap'] ?? gpu['power_limit'] ?? gpu['max_power'];\n      final power = _formatPower(powerDraw, powerCap);\n\n      // Memory parsing\n      final memory = _parseMemory(gpu['memory'] ?? gpu['vram'] ?? {});\n\n      // Utilization parsing\n      final utilization = _parseIntValue(gpu['utilization'] ?? gpu['gpu_util'] ?? gpu['activity']);\n\n      // Fan speed parsing\n      final fanSpeed = _parseIntValue(gpu['fan_speed'] ?? gpu['fan_rpm']);\n\n      // Clock speed parsing\n      final clockSpeed = _parseIntValue(gpu['clock_speed'] ?? gpu['gpu_clock'] ?? gpu['sclk']);\n\n      return AmdSmiItem(\n        deviceId: deviceId,\n        name: name,\n        temp: temp,\n        power: power,\n        memory: memory,\n        utilization: utilization,\n        fanSpeed: fanSpeed,\n        clockSpeed: clockSpeed,\n      );\n    } catch (e) {\n      return null;\n    }\n  }\n\n  static int _parseIntValue(dynamic value) {\n    if (value == null) return 0;\n    if (value is int) return value;\n    if (value is String) {\n      // Remove units and parse (e.g., \"45°C\" -> 45, \"1200 RPM\" -> 1200)\n      final cleanValue = value.replaceAll(RegExp(r'[^\\d]'), '');\n      return int.tryParse(cleanValue) ?? 0;\n    }\n    return 0;\n  }\n\n  static String _formatPower(dynamic draw, dynamic cap) {\n    final drawValue = _parseIntValue(draw);\n    final capValue = _parseIntValue(cap);\n\n    if (drawValue == 0 && capValue == 0) return 'N/A';\n    if (capValue == 0) return '${drawValue}W';\n    return '${drawValue}W / ${capValue}W';\n  }\n\n  static AmdSmiMem _parseMemory(Map<String, dynamic> memData) {\n    final total = _parseIntValue(memData['total'] ?? memData['total_memory']);\n    final used = _parseIntValue(memData['used'] ?? memData['used_memory']);\n    final unit = memData['unit']?.toString() ?? 'MB';\n\n    final processes = <AmdSmiMemProcess>[];\n    final processesData = memData['processes'];\n    if (processesData is List) {\n      for (final proc in processesData) {\n        if (proc is Map<String, dynamic>) {\n          final process = _parseProcess(proc);\n          if (process != null) processes.add(process);\n        }\n      }\n    }\n\n    return AmdSmiMem(total, used, unit, processes);\n  }\n\n  static AmdSmiMemProcess? _parseProcess(Map<String, dynamic> procData) {\n    final pid = _parseIntValue(procData['pid']);\n    final name = procData['name']?.toString() ?? procData['process_name']?.toString() ?? 'Unknown';\n    final memory = _parseIntValue(procData['memory'] ?? procData['used_memory']);\n\n    if (pid == 0) return null;\n    return AmdSmiMemProcess(pid, name, memory);\n  }\n}\n\nclass AmdSmiItem {\n  final String deviceId;\n  final String name;\n  final int temp;\n  final String power;\n  final AmdSmiMem memory;\n  final int utilization;\n  final int fanSpeed;\n  final int clockSpeed;\n\n  const AmdSmiItem({\n    required this.deviceId,\n    required this.name,\n    required this.temp,\n    required this.power,\n    required this.memory,\n    required this.utilization,\n    required this.fanSpeed,\n    required this.clockSpeed,\n  });\n\n  @override\n  String toString() {\n    return 'AmdSmiItem{name: $name, temp: $temp, power: $power, utilization: $utilization%, memory: $memory}';\n  }\n}\n\nclass AmdSmiMem {\n  final int total;\n  final int used;\n  final String unit;\n  final List<AmdSmiMemProcess> processes;\n\n  const AmdSmiMem(this.total, this.used, this.unit, this.processes);\n\n  @override\n  String toString() {\n    return 'AmdSmiMem{total: $total, used: $used, unit: $unit, processes: ${processes.length}}';\n  }\n}\n\nclass AmdSmiMemProcess {\n  final int pid;\n  final String name;\n  final int memory;\n\n  const AmdSmiMemProcess(this.pid, this.name, this.memory);\n\n  @override\n  String toString() {\n    return 'AmdSmiMemProcess{pid: $pid, name: $name, memory: $memory}';\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/battery.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\n\n/// raw dat from server:\n/// ```text\n/// POWER_SUPPLY_NAME=hidpp_battery_0\n/// POWER_SUPPLY_TYPE=Battery\n/// POWER_SUPPLY_ONLINE=1\n/// POWER_SUPPLY_STATUS=Discharging\n/// POWER_SUPPLY_SCOPE=Device\n/// POWER_SUPPLY_MODEL_NAME=MX Anywhere 3\n/// POWER_SUPPLY_MANUFACTURER=Logitech\n/// POWER_SUPPLY_SERIAL_NUMBER=0f-fc-43-f8\n/// POWER_SUPPLY_CAPACITY=35\n/// ```\nclass Battery {\n  final int? percent;\n  final BatteryStatus status;\n  final String? name;\n  final int? cycle;\n  final String? tech;\n\n  const Battery({required this.status, this.percent, this.name, this.cycle, this.tech});\n\n  factory Battery.fromRaw(String raw) {\n    final lines = raw.split('\\n');\n    final map = <String, String>{};\n    for (final line in lines) {\n      final parts = line.split('=');\n      if (parts.length != 2) continue;\n      map[parts[0]] = parts[1];\n    }\n\n    final capacity = map['POWER_SUPPLY_CAPACITY']; // 30%\n    final cycle = map['POWER_SUPPLY_CYCLE_COUNT']; // 30\n\n    var name = map['POWER_SUPPLY_MODEL_NAME'];\n    name ??= map['POWER_SUPPLY_NAME'];\n\n    return Battery(\n      percent: capacity == null ? null : int.tryParse(capacity),\n      status: BatteryStatus.parse(map['POWER_SUPPLY_STATUS']),\n      name: name,\n      cycle: cycle == null ? null : int.tryParse(cycle),\n      tech: map['POWER_SUPPLY_TECHNOLOGY'],\n    );\n  }\n\n  @override\n  String toString() {\n    return 'Battery{$percent, $status, $name, $cycle}';\n  }\n\n  bool get isLiPoly => tech == 'Li-poly';\n}\n\nenum BatteryStatus {\n  charging,\n  discharging,\n  full,\n  unknown;\n\n  static BatteryStatus parse(String? status) {\n    switch (status) {\n      case 'Charging':\n        return BatteryStatus.charging;\n      case 'Discharging':\n        return BatteryStatus.discharging;\n      case 'Full':\n        return BatteryStatus.full;\n      default:\n        return BatteryStatus.unknown;\n    }\n  }\n}\n\nabstract final class Batteries {\n  static List<Battery> parse(String raw, [bool onlyLiPoly = false]) {\n    final lines = raw.split('\\n');\n    final batteries = <Battery>[];\n    final oneBatLines = <String>[];\n    for (final line in lines) {\n      if (line.isEmpty) {\n        try {\n          final bat = Battery.fromRaw(oneBatLines.join('\\n'));\n          if (onlyLiPoly && !bat.isLiPoly) continue;\n          batteries.add(bat);\n        } catch (e, s) {\n          Loggers.app.warning(e, s);\n        }\n        oneBatLines.clear();\n      } else {\n        oneBatLines.add(line);\n      }\n    }\n    return batteries;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/conn.dart",
    "content": "import 'package:server_box/data/res/misc.dart';\n\nclass Conn {\n  final int maxConn;\n  final int active;\n  final int passive;\n  final int fail;\n\n  const Conn({required this.maxConn, required this.active, required this.passive, required this.fail});\n\n  static Conn? parse(String raw) {\n    final lines = raw.split('\\n');\n    final idx = lines.lastWhere((element) => element.startsWith('Tcp:'), orElse: () => '');\n    if (idx != '') {\n      final vals = idx.split(Miscs.blankReg);\n      return Conn(\n        maxConn: int.tryParse(vals[5]) ?? 0,\n        active: int.tryParse(vals[6]) ?? 0,\n        passive: int.tryParse(vals[7]) ?? 0,\n        fail: int.tryParse(vals[8]) ?? 0,\n      );\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/connection_stat.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:hive_ce/hive.dart';\n\npart 'connection_stat.freezed.dart';\npart 'connection_stat.g.dart';\n\n@freezed\n@HiveType(typeId: 100)\nabstract class ConnectionStat with _$ConnectionStat {\n  const factory ConnectionStat({\n    @HiveField(0) required String serverId,\n    @HiveField(1) required String serverName,\n    @HiveField(2) required DateTime timestamp,\n    @HiveField(3) required ConnectionResult result,\n    @HiveField(4) @Default('') String errorMessage,\n    @HiveField(5) required int durationMs,\n  }) = _ConnectionStat;\n\n  factory ConnectionStat.fromJson(Map<String, dynamic> json) =>\n      _$ConnectionStatFromJson(json);\n}\n\n@freezed\n@HiveType(typeId: 101)\nabstract class ServerConnectionStats with _$ServerConnectionStats {\n  const factory ServerConnectionStats({\n    @HiveField(0) required String serverId,\n    @HiveField(1) required String serverName,\n    @HiveField(2) required int totalAttempts,\n    @HiveField(3) required int successCount,\n    @HiveField(4) required int failureCount,\n    @HiveField(5) @Default(null) DateTime? lastSuccessTime,\n    @HiveField(6) @Default(null) DateTime? lastFailureTime,\n    @HiveField(7) @Default([]) List<ConnectionStat> recentConnections,\n    @HiveField(8) required double successRate,\n  }) = _ServerConnectionStats;\n\n  factory ServerConnectionStats.fromJson(Map<String, dynamic> json) =>\n      _$ServerConnectionStatsFromJson(json);\n}\n\n@HiveType(typeId: 102)\nenum ConnectionResult {\n  @HiveField(0)\n  @JsonValue('success')\n  success,\n  @HiveField(1)\n  @JsonValue('timeout')\n  timeout,\n  @HiveField(2)\n  @JsonValue('auth_failed')\n  authFailed,\n  @HiveField(3)\n  @JsonValue('network_error')\n  networkError,\n  @HiveField(4)\n  @JsonValue('unknown_error')\n  unknownError,\n}\n\nextension ConnectionResultExtension on ConnectionResult {\n  String get displayName {\n    switch (this) {\n      case ConnectionResult.success:\n        return libL10n.success;\n      case ConnectionResult.timeout:\n        return '${libL10n.error}(timeout)';\n      case ConnectionResult.authFailed:\n        return '${libL10n.error}(auth)';\n      case ConnectionResult.networkError:\n        return '${libL10n.error}(${libL10n.network})';\n      case ConnectionResult.unknownError:\n        return '${libL10n.error}(${libL10n.unknown})';\n    }\n  }\n\n  bool get isSuccess => this == ConnectionResult.success;\n}"
  },
  {
    "path": "lib/data/model/server/connection_stat.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'connection_stat.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n\n/// @nodoc\nmixin _$ConnectionStat {\n\n@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) DateTime get timestamp;@HiveField(3) ConnectionResult get result;@HiveField(4) String get errorMessage;@HiveField(5) int get durationMs;\n/// Create a copy of ConnectionStat\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$ConnectionStatCopyWith<ConnectionStat> get copyWith => _$ConnectionStatCopyWithImpl<ConnectionStat>(this as ConnectionStat, _$identity);\n\n  /// Serializes this ConnectionStat to a JSON map.\n  Map<String, dynamic> toJson();\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);\n\n@override\nString toString() {\n  return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $ConnectionStatCopyWith<$Res>  {\n  factory $ConnectionStatCopyWith(ConnectionStat value, $Res Function(ConnectionStat) _then) = _$ConnectionStatCopyWithImpl;\n@useResult\n$Res call({\n@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs\n});\n\n\n\n\n}\n/// @nodoc\nclass _$ConnectionStatCopyWithImpl<$Res>\n    implements $ConnectionStatCopyWith<$Res> {\n  _$ConnectionStatCopyWithImpl(this._self, this._then);\n\n  final ConnectionStat _self;\n  final $Res Function(ConnectionStat) _then;\n\n/// Create a copy of ConnectionStat\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {\n  return _then(_self.copyWith(\nserverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable\nas String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable\nas String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable\nas DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable\nas ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable\nas String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable\nas int,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [ConnectionStat].\nextension ConnectionStatPatterns on ConnectionStat {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ConnectionStat value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _ConnectionStat() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ConnectionStat value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _ConnectionStat():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ConnectionStat value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _ConnectionStat() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0)  String serverId, @HiveField(1)  String serverName, @HiveField(2)  DateTime timestamp, @HiveField(3)  ConnectionResult result, @HiveField(4)  String errorMessage, @HiveField(5)  int durationMs)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _ConnectionStat() when $default != null:\nreturn $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0)  String serverId, @HiveField(1)  String serverName, @HiveField(2)  DateTime timestamp, @HiveField(3)  ConnectionResult result, @HiveField(4)  String errorMessage, @HiveField(5)  int durationMs)  $default,) {final _that = this;\nswitch (_that) {\ncase _ConnectionStat():\nreturn $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0)  String serverId, @HiveField(1)  String serverName, @HiveField(2)  DateTime timestamp, @HiveField(3)  ConnectionResult result, @HiveField(4)  String errorMessage, @HiveField(5)  int durationMs)?  $default,) {final _that = this;\nswitch (_that) {\ncase _ConnectionStat() when $default != null:\nreturn $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n@JsonSerializable()\n\nclass _ConnectionStat implements ConnectionStat {\n  const _ConnectionStat({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.timestamp, @HiveField(3) required this.result, @HiveField(4) this.errorMessage = '', @HiveField(5) required this.durationMs});\n  factory _ConnectionStat.fromJson(Map<String, dynamic> json) => _$ConnectionStatFromJson(json);\n\n@override@HiveField(0) final  String serverId;\n@override@HiveField(1) final  String serverName;\n@override@HiveField(2) final  DateTime timestamp;\n@override@HiveField(3) final  ConnectionResult result;\n@override@JsonKey()@HiveField(4) final  String errorMessage;\n@override@HiveField(5) final  int durationMs;\n\n/// Create a copy of ConnectionStat\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$ConnectionStatCopyWith<_ConnectionStat> get copyWith => __$ConnectionStatCopyWithImpl<_ConnectionStat>(this, _$identity);\n\n@override\nMap<String, dynamic> toJson() {\n  return _$ConnectionStatToJson(this, );\n}\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);\n\n@override\nString toString() {\n  return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$ConnectionStatCopyWith<$Res> implements $ConnectionStatCopyWith<$Res> {\n  factory _$ConnectionStatCopyWith(_ConnectionStat value, $Res Function(_ConnectionStat) _then) = __$ConnectionStatCopyWithImpl;\n@override @useResult\n$Res call({\n@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs\n});\n\n\n\n\n}\n/// @nodoc\nclass __$ConnectionStatCopyWithImpl<$Res>\n    implements _$ConnectionStatCopyWith<$Res> {\n  __$ConnectionStatCopyWithImpl(this._self, this._then);\n\n  final _ConnectionStat _self;\n  final $Res Function(_ConnectionStat) _then;\n\n/// Create a copy of ConnectionStat\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {\n  return _then(_ConnectionStat(\nserverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable\nas String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable\nas String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable\nas DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable\nas ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable\nas String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable\nas int,\n  ));\n}\n\n\n}\n\n\n/// @nodoc\nmixin _$ServerConnectionStats {\n\n@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) int get totalAttempts;@HiveField(3) int get successCount;@HiveField(4) int get failureCount;@HiveField(5) DateTime? get lastSuccessTime;@HiveField(6) DateTime? get lastFailureTime;@HiveField(7) List<ConnectionStat> get recentConnections;@HiveField(8) double get successRate;\n/// Create a copy of ServerConnectionStats\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$ServerConnectionStatsCopyWith<ServerConnectionStats> get copyWith => _$ServerConnectionStatsCopyWithImpl<ServerConnectionStats>(this as ServerConnectionStats, _$identity);\n\n  /// Serializes this ServerConnectionStats to a JSON map.\n  Map<String, dynamic> toJson();\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other.recentConnections, recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(recentConnections),successRate);\n\n@override\nString toString() {\n  return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $ServerConnectionStatsCopyWith<$Res>  {\n  factory $ServerConnectionStatsCopyWith(ServerConnectionStats value, $Res Function(ServerConnectionStats) _then) = _$ServerConnectionStatsCopyWithImpl;\n@useResult\n$Res call({\n@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate\n});\n\n\n\n\n}\n/// @nodoc\nclass _$ServerConnectionStatsCopyWithImpl<$Res>\n    implements $ServerConnectionStatsCopyWith<$Res> {\n  _$ServerConnectionStatsCopyWithImpl(this._self, this._then);\n\n  final ServerConnectionStats _self;\n  final $Res Function(ServerConnectionStats) _then;\n\n/// Create a copy of ServerConnectionStats\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {\n  return _then(_self.copyWith(\nserverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable\nas String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable\nas String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable\nas int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable\nas int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable\nas int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable\nas DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable\nas DateTime?,recentConnections: null == recentConnections ? _self.recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable\nas List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable\nas double,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [ServerConnectionStats].\nextension ServerConnectionStatsPatterns on ServerConnectionStats {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServerConnectionStats value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _ServerConnectionStats() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServerConnectionStats value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _ServerConnectionStats():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServerConnectionStats value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _ServerConnectionStats() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0)  String serverId, @HiveField(1)  String serverName, @HiveField(2)  int totalAttempts, @HiveField(3)  int successCount, @HiveField(4)  int failureCount, @HiveField(5)  DateTime? lastSuccessTime, @HiveField(6)  DateTime? lastFailureTime, @HiveField(7)  List<ConnectionStat> recentConnections, @HiveField(8)  double successRate)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _ServerConnectionStats() when $default != null:\nreturn $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0)  String serverId, @HiveField(1)  String serverName, @HiveField(2)  int totalAttempts, @HiveField(3)  int successCount, @HiveField(4)  int failureCount, @HiveField(5)  DateTime? lastSuccessTime, @HiveField(6)  DateTime? lastFailureTime, @HiveField(7)  List<ConnectionStat> recentConnections, @HiveField(8)  double successRate)  $default,) {final _that = this;\nswitch (_that) {\ncase _ServerConnectionStats():\nreturn $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0)  String serverId, @HiveField(1)  String serverName, @HiveField(2)  int totalAttempts, @HiveField(3)  int successCount, @HiveField(4)  int failureCount, @HiveField(5)  DateTime? lastSuccessTime, @HiveField(6)  DateTime? lastFailureTime, @HiveField(7)  List<ConnectionStat> recentConnections, @HiveField(8)  double successRate)?  $default,) {final _that = this;\nswitch (_that) {\ncase _ServerConnectionStats() when $default != null:\nreturn $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n@JsonSerializable()\n\nclass _ServerConnectionStats implements ServerConnectionStats {\n  const _ServerConnectionStats({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.totalAttempts, @HiveField(3) required this.successCount, @HiveField(4) required this.failureCount, @HiveField(5) this.lastSuccessTime = null, @HiveField(6) this.lastFailureTime = null, @HiveField(7) final  List<ConnectionStat> recentConnections = const [], @HiveField(8) required this.successRate}): _recentConnections = recentConnections;\n  factory _ServerConnectionStats.fromJson(Map<String, dynamic> json) => _$ServerConnectionStatsFromJson(json);\n\n@override@HiveField(0) final  String serverId;\n@override@HiveField(1) final  String serverName;\n@override@HiveField(2) final  int totalAttempts;\n@override@HiveField(3) final  int successCount;\n@override@HiveField(4) final  int failureCount;\n@override@JsonKey()@HiveField(5) final  DateTime? lastSuccessTime;\n@override@JsonKey()@HiveField(6) final  DateTime? lastFailureTime;\n final  List<ConnectionStat> _recentConnections;\n@override@JsonKey()@HiveField(7) List<ConnectionStat> get recentConnections {\n  if (_recentConnections is EqualUnmodifiableListView) return _recentConnections;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(_recentConnections);\n}\n\n@override@HiveField(8) final  double successRate;\n\n/// Create a copy of ServerConnectionStats\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$ServerConnectionStatsCopyWith<_ServerConnectionStats> get copyWith => __$ServerConnectionStatsCopyWithImpl<_ServerConnectionStats>(this, _$identity);\n\n@override\nMap<String, dynamic> toJson() {\n  return _$ServerConnectionStatsToJson(this, );\n}\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other._recentConnections, _recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(_recentConnections),successRate);\n\n@override\nString toString() {\n  return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$ServerConnectionStatsCopyWith<$Res> implements $ServerConnectionStatsCopyWith<$Res> {\n  factory _$ServerConnectionStatsCopyWith(_ServerConnectionStats value, $Res Function(_ServerConnectionStats) _then) = __$ServerConnectionStatsCopyWithImpl;\n@override @useResult\n$Res call({\n@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate\n});\n\n\n\n\n}\n/// @nodoc\nclass __$ServerConnectionStatsCopyWithImpl<$Res>\n    implements _$ServerConnectionStatsCopyWith<$Res> {\n  __$ServerConnectionStatsCopyWithImpl(this._self, this._then);\n\n  final _ServerConnectionStats _self;\n  final $Res Function(_ServerConnectionStats) _then;\n\n/// Create a copy of ServerConnectionStats\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {\n  return _then(_ServerConnectionStats(\nserverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable\nas String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable\nas String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable\nas int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable\nas int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable\nas int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable\nas DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable\nas DateTime?,recentConnections: null == recentConnections ? _self._recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable\nas List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable\nas double,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/model/server/connection_stat.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'connection_stat.dart';\n\n// **************************************************************************\n// TypeAdapterGenerator\n// **************************************************************************\n\nclass ConnectionStatAdapter extends TypeAdapter<ConnectionStat> {\n  @override\n  final typeId = 100;\n\n  @override\n  ConnectionStat read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return ConnectionStat(\n      serverId: fields[0] as String,\n      serverName: fields[1] as String,\n      timestamp: fields[2] as DateTime,\n      result: fields[3] as ConnectionResult,\n      errorMessage: fields[4] == null ? '' : fields[4] as String,\n      durationMs: (fields[5] as num).toInt(),\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, ConnectionStat obj) {\n    writer\n      ..writeByte(6)\n      ..writeByte(0)\n      ..write(obj.serverId)\n      ..writeByte(1)\n      ..write(obj.serverName)\n      ..writeByte(2)\n      ..write(obj.timestamp)\n      ..writeByte(3)\n      ..write(obj.result)\n      ..writeByte(4)\n      ..write(obj.errorMessage)\n      ..writeByte(5)\n      ..write(obj.durationMs);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is ConnectionStatAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass ServerConnectionStatsAdapter extends TypeAdapter<ServerConnectionStats> {\n  @override\n  final typeId = 101;\n\n  @override\n  ServerConnectionStats read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return ServerConnectionStats(\n      serverId: fields[0] as String,\n      serverName: fields[1] as String,\n      totalAttempts: (fields[2] as num).toInt(),\n      successCount: (fields[3] as num).toInt(),\n      failureCount: (fields[4] as num).toInt(),\n      lastSuccessTime: fields[5] == null ? null : fields[5] as DateTime?,\n      lastFailureTime: fields[6] == null ? null : fields[6] as DateTime?,\n      recentConnections: fields[7] == null\n          ? []\n          : (fields[7] as List).cast<ConnectionStat>(),\n      successRate: (fields[8] as num).toDouble(),\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, ServerConnectionStats obj) {\n    writer\n      ..writeByte(9)\n      ..writeByte(0)\n      ..write(obj.serverId)\n      ..writeByte(1)\n      ..write(obj.serverName)\n      ..writeByte(2)\n      ..write(obj.totalAttempts)\n      ..writeByte(3)\n      ..write(obj.successCount)\n      ..writeByte(4)\n      ..write(obj.failureCount)\n      ..writeByte(5)\n      ..write(obj.lastSuccessTime)\n      ..writeByte(6)\n      ..write(obj.lastFailureTime)\n      ..writeByte(7)\n      ..write(obj.recentConnections)\n      ..writeByte(8)\n      ..write(obj.successRate);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is ServerConnectionStatsAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass ConnectionResultAdapter extends TypeAdapter<ConnectionResult> {\n  @override\n  final typeId = 102;\n\n  @override\n  ConnectionResult read(BinaryReader reader) {\n    switch (reader.readByte()) {\n      case 0:\n        return ConnectionResult.success;\n      case 1:\n        return ConnectionResult.timeout;\n      case 2:\n        return ConnectionResult.authFailed;\n      case 3:\n        return ConnectionResult.networkError;\n      case 4:\n        return ConnectionResult.unknownError;\n      default:\n        return ConnectionResult.success;\n    }\n  }\n\n  @override\n  void write(BinaryWriter writer, ConnectionResult obj) {\n    switch (obj) {\n      case ConnectionResult.success:\n        writer.writeByte(0);\n      case ConnectionResult.timeout:\n        writer.writeByte(1);\n      case ConnectionResult.authFailed:\n        writer.writeByte(2);\n      case ConnectionResult.networkError:\n        writer.writeByte(3);\n      case ConnectionResult.unknownError:\n        writer.writeByte(4);\n    }\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is ConnectionResultAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\n// **************************************************************************\n// JsonSerializableGenerator\n// **************************************************************************\n\n_ConnectionStat _$ConnectionStatFromJson(Map<String, dynamic> json) =>\n    _ConnectionStat(\n      serverId: json['serverId'] as String,\n      serverName: json['serverName'] as String,\n      timestamp: DateTime.parse(json['timestamp'] as String),\n      result: $enumDecode(_$ConnectionResultEnumMap, json['result']),\n      errorMessage: json['errorMessage'] as String? ?? '',\n      durationMs: (json['durationMs'] as num).toInt(),\n    );\n\nMap<String, dynamic> _$ConnectionStatToJson(_ConnectionStat instance) =>\n    <String, dynamic>{\n      'serverId': instance.serverId,\n      'serverName': instance.serverName,\n      'timestamp': instance.timestamp.toIso8601String(),\n      'result': _$ConnectionResultEnumMap[instance.result]!,\n      'errorMessage': instance.errorMessage,\n      'durationMs': instance.durationMs,\n    };\n\nconst _$ConnectionResultEnumMap = {\n  ConnectionResult.success: 'success',\n  ConnectionResult.timeout: 'timeout',\n  ConnectionResult.authFailed: 'auth_failed',\n  ConnectionResult.networkError: 'network_error',\n  ConnectionResult.unknownError: 'unknown_error',\n};\n\n_ServerConnectionStats _$ServerConnectionStatsFromJson(\n  Map<String, dynamic> json,\n) => _ServerConnectionStats(\n  serverId: json['serverId'] as String,\n  serverName: json['serverName'] as String,\n  totalAttempts: (json['totalAttempts'] as num).toInt(),\n  successCount: (json['successCount'] as num).toInt(),\n  failureCount: (json['failureCount'] as num).toInt(),\n  lastSuccessTime: json['lastSuccessTime'] == null\n      ? null\n      : DateTime.parse(json['lastSuccessTime'] as String),\n  lastFailureTime: json['lastFailureTime'] == null\n      ? null\n      : DateTime.parse(json['lastFailureTime'] as String),\n  recentConnections:\n      (json['recentConnections'] as List<dynamic>?)\n          ?.map((e) => ConnectionStat.fromJson(e as Map<String, dynamic>))\n          .toList() ??\n      const [],\n  successRate: (json['successRate'] as num).toDouble(),\n);\n\nMap<String, dynamic> _$ServerConnectionStatsToJson(\n  _ServerConnectionStats instance,\n) => <String, dynamic>{\n  'serverId': instance.serverId,\n  'serverName': instance.serverName,\n  'totalAttempts': instance.totalAttempts,\n  'successCount': instance.successCount,\n  'failureCount': instance.failureCount,\n  'lastSuccessTime': instance.lastSuccessTime?.toIso8601String(),\n  'lastFailureTime': instance.lastFailureTime?.toIso8601String(),\n  'recentConnections': instance.recentConnections,\n  'successRate': instance.successRate,\n};\n"
  },
  {
    "path": "lib/data/model/server/cpu.dart",
    "content": "import 'package:fl_chart/fl_chart.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/server/time_seq.dart';\nimport 'package:server_box/data/res/status.dart';\n\n/// Capacity of the FIFO queue\nconst _kCap = 30;\n\nclass Cpus extends TimeSeq<SingleCpuCore> {\n  Cpus(super.init1, super.init2);\n\n  final Map<String, int> brand = {};\n\n  @override\n  void onUpdate() {\n    _coresCount = now.length;\n    if (pre.isEmpty || now.isEmpty || pre.length != now.length) {\n      _totalDelta = 0;\n      _user = 0;\n      _sys = 0;\n      _iowait = 0;\n      _idle = 0;\n      return;\n    }\n    _totalDelta = now[0].total - pre[0].total;\n    _user = _getUser();\n    _sys = _getSys();\n    _iowait = _getIowait();\n    _idle = _getIdle();\n    _updateSpots();\n  }\n\n  double usedPercent({int coreIdx = 0}) {\n    if (now.length != pre.length) return 0;\n    if (now.isEmpty) return 0;\n    if (coreIdx >= now.length) return 0;\n    try {\n      final idleDelta = now[coreIdx].idle - pre[coreIdx].idle;\n      final totalDelta = now[coreIdx].total - pre[coreIdx].total;\n      if (totalDelta == 0) return 0;\n      final used = idleDelta / totalDelta;\n      return used.isNaN ? 0 : 100 - used * 100;\n    } catch (e, s) {\n      Loggers.app.warning('Cpus.usedPercent()', e, s);\n      return 0;\n    }\n  }\n\n  int _coresCount = 0;\n  int get coresCount => _coresCount;\n\n  int _totalDelta = 0;\n  int get totalDelta => _totalDelta;\n\n  double _user = 0;\n  double get user => _user;\n  double _getUser() {\n    if (now.length != pre.length) return 0;\n    final delta = now[0].user - pre[0].user;\n    final used = delta / totalDelta;\n    return used.isNaN ? 0 : used * 100;\n  }\n\n  double _sys = 0;\n  double get sys => _sys;\n  double _getSys() {\n    if (now.length != pre.length) return 0;\n    final delta = now[0].sys - pre[0].sys;\n    final used = delta / totalDelta;\n    return used.isNaN ? 0 : used * 100;\n  }\n\n  double _iowait = 0;\n  double get iowait => _iowait;\n  double _getIowait() {\n    if (now.length != pre.length) return 0;\n    final delta = now[0].iowait - pre[0].iowait;\n    final used = delta / totalDelta;\n    return used.isNaN ? 0 : used * 100;\n  }\n\n  double _idle = 0;\n  double get idle => _idle;\n  double _getIdle() => 100 - usedPercent();\n\n  void _coresLoop(void Function(int i) callback) {\n    /// Only update the entire cpu when [coresCount] > 4, or the chart will be too crowded\n    // final onlyCalcSingle = coresCount > 4;\n    // final maxIdx = onlyCalcSingle ? 1 : coresCount;\n    // for (var i = onlyCalcSingle ? 0 : 1; i < maxIdx; i++) {\n    //   callback(i);\n    // }\n\n    /// Only use cpu0\n    callback(0);\n  }\n\n  /// [core1, core2]\n  /// core1: [FlSpot(0, 10), FlSpot(1, 20), FlSpot(2, 30)]\n  final _spots = <Fifo<FlSpot>>[];\n  List<Fifo<FlSpot>> get spots => _spots;\n  void _updateSpots() {\n    _coresLoop((i) {\n      if (i >= _spots.length) {\n        _spots.add(Fifo(capacity: _kCap));\n      } else {\n        final item = _spots[i];\n        final spot = FlSpot(item.count.toDouble(), usedPercent(coreIdx: i));\n        item.add(spot);\n      }\n    });\n  }\n\n  // var _rangeX = Range<double>(0.0, _kCap.toDouble());\n  // Range<double> get rangeX => _rangeX;\n  // // var _rangeY = Range<double>(0.0, 100.0);\n  // // Range<double> get rangeY => _rangeY;\n  // void _updateRange() {\n  //   double minX = 0;\n  //   double maxX = 0;\n  //   _coresLoop((i) {\n  //     final fifo = _spots[i];\n  //     if (fifo.isEmpty) return;\n  //     final first = fifo.first.x;\n  //     final last = fifo.last.x;\n  //     if (first > minX) minX = first;\n  //     if (last > maxX) maxX = last;\n  //   });\n  //   _rangeX = Range(minX, maxX);\n\n  //   // double? minY, maxY;\n  //   // for (var i = 1; i < now.length; i++) {\n  //   //   final item = _spots[i];\n  //   //   if (item.isEmpty) continue;\n  //   //   final first = item.first.y;\n  //   //   final last = item.last.y;\n  //   //   if (minY == null || first < minY) minY = first;\n  //   //   if (maxY == null || last > maxY) maxY = last;\n  //   // }\n  //   // if (minY != null && maxY != null) _rangeY = Range(minY, maxY);\n  // }\n}\n\nclass SingleCpuCore extends TimeSeqIface<SingleCpuCore> {\n  final String id;\n  final int user;\n  final int sys;\n  final int nice;\n  final int idle;\n  final int iowait;\n  final int irq;\n  final int softirq;\n\n  SingleCpuCore(this.id, this.user, this.sys, this.nice, this.idle, this.iowait, this.irq, this.softirq);\n\n  int get total => user + sys + nice + idle + iowait + irq + softirq;\n\n  @override\n  bool same(SingleCpuCore other) => id == other.id;\n\n  static List<SingleCpuCore> parse(String raw) {\n    final List<SingleCpuCore> cpus = [];\n\n    for (var item in raw.split('\\n')) {\n      if (item == '') break;\n      final id = item.split(' ').firstOrNull;\n      if (id == null) continue;\n      final matches = item.replaceFirst(id, '').trim().split(' ');\n      if (matches.length < 7) continue;\n      cpus.add(\n        SingleCpuCore(\n          id,\n          int.parse(matches[0]),\n          int.parse(matches[1]),\n          int.parse(matches[2]),\n          int.parse(matches[3]),\n          int.parse(matches[4]),\n          int.parse(matches[5]),\n          int.parse(matches[6]),\n        ),\n      );\n    }\n    return cpus;\n  }\n}\n\nfinal class CpuBrand {\n  static Map<String, int> parse(String raw) {\n    final lines = raw.split('\\n');\n    // {brand: count}\n    final brands = <String, int>{};\n    for (var line in lines) {\n      if (line.contains('model name')) {\n        final model = line.split(':').last.trim();\n        final count = brands[model] ?? 0;\n        brands[model] = count + 1;\n      }\n    }\n    return brands;\n  }\n}\n\nfinal _bsdCpuPercentReg = RegExp(r'(\\d+\\.\\d+)%');\nfinal _macCpuPercentReg = RegExp(r'CPU usage: ([\\d.]+)% user, ([\\d.]+)% sys, ([\\d.]+)% idle');\nfinal _freebsdCpuPercentReg = RegExp(\n  r'CPU: ([\\d.]+)% user, ([\\d.]+)% nice, ([\\d.]+)% system, '\n  r'([\\d.]+)% interrupt, ([\\d.]+)% idle',\n);\n\n/// Parse CPU status on BSD system with support for different BSD variants\n///\n/// Supports multiple formats:\n/// - macOS: \"CPU usage: 14.70% user, 12.76% sys, 72.52% idle\"\n/// - FreeBSD: \"CPU: 5.2% user, 0.0% nice, 3.1% system, 0.1% interrupt, 91.6% idle\"\n/// - Generic BSD: fallback to percentage extraction\nCpus parseBsdCpu(String raw) {\n  final init = InitStatus.cpus;\n\n  // Try macOS format first\n  final macMatch = _macCpuPercentReg.firstMatch(raw);\n  if (macMatch != null) {\n    final userPercent = double.parse(macMatch.group(1)!).toInt();\n    final sysPercent = double.parse(macMatch.group(2)!).toInt();\n    final idlePercent = double.parse(macMatch.group(3)!).toInt();\n\n    init.add([\n      SingleCpuCore(\n        'cpu0',\n        userPercent,\n        sysPercent,\n        0, // nice\n        idlePercent,\n        0, // iowait\n        0, // irq\n        0, // softirq\n      ),\n    ]);\n    return init;\n  }\n\n  // Try FreeBSD format\n  final freebsdMatch = _freebsdCpuPercentReg.firstMatch(raw);\n  if (freebsdMatch != null) {\n    final userPercent = double.parse(freebsdMatch.group(1)!).toInt();\n    final nicePercent = double.parse(freebsdMatch.group(2)!).toInt();\n    final sysPercent = double.parse(freebsdMatch.group(3)!).toInt();\n    final irqPercent = double.parse(freebsdMatch.group(4)!).toInt();\n    final idlePercent = double.parse(freebsdMatch.group(5)!).toInt();\n\n    init.add([\n      SingleCpuCore(\n        'cpu0',\n        userPercent,\n        sysPercent,\n        nicePercent,\n        idlePercent,\n        0, // iowait\n        irqPercent,\n        0, // softirq\n      ),\n    ]);\n    return init;\n  }\n\n  // Fallback to generic percentage extraction\n  final percents = _bsdCpuPercentReg\n      .allMatches(raw)\n      .map((e) {\n        final valueStr = e.group(1) ?? '0';\n        final value = double.tryParse(valueStr);\n        if (value == null) {\n          dprint('Warning: Failed to parse CPU percentage from \"$valueStr\"');\n          return 0.0;\n        }\n        return value;\n      })\n      .toList();\n\n  if (percents.length >= 3) {\n    // Validate that percentages are reasonable (0-100 range)\n    final validPercents = percents.where((p) => p >= 0 && p <= 100).toList();\n    if (validPercents.length != percents.length) {\n      Loggers.app.warning('BSD CPU fallback parsing found invalid percentages in: $raw');\n    }\n\n    init.add([\n      SingleCpuCore(\n        'cpu0',\n        percents[0].toInt(), // user\n        percents.length > 1 ? percents[1].toInt() : 0, // sys\n        0, // nice\n        percents.length > 2 ? percents[2].toInt() : 0, // idle\n        0, // iowait\n        0, // irq\n        0, // softirq\n      ),\n    ]);\n    return init;\n  } else if (percents.isNotEmpty) {\n    Loggers.app.warning(\n      'BSD CPU fallback parsing found ${percents.length} percentages (expected at least 3) in: $raw',\n    );\n  } else {\n    Loggers.app.warning('BSD CPU fallback parsing found no percentages in: $raw');\n  }\n\n  return init;\n}\n"
  },
  {
    "path": "lib/data/model/server/custom.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'custom.g.dart';\n\n@JsonSerializable(includeIfNull: false)\nfinal class ServerCustom {\n  // @HiveField(0)\n  // final String? temperature;\n\n  final String? pveAddr;\n\n  final bool pveIgnoreCert;\n\n  /// {\"title\": \"cmd\"}\n  final Map<String, String>? cmds;\n\n  final String? preferTempDev;\n\n  final String? logoUrl;\n\n  /// The device name of the network interface displayed in the home server card.\n  final String? netDev;\n\n  /// The directory where the script is stored.\n  final String? scriptDir;\n\n  const ServerCustom({\n    //this.temperature,\n    this.pveAddr,\n    this.pveIgnoreCert = false,\n    this.cmds,\n    this.preferTempDev,\n    this.logoUrl,\n    this.netDev,\n    this.scriptDir,\n  });\n\n  factory ServerCustom.fromJson(Map<String, dynamic> json) => _$ServerCustomFromJson(json);\n\n  Map<String, dynamic> toJson() => _$ServerCustomToJson(this);\n\n  @override\n  bool operator ==(Object other) {\n    return other is ServerCustom &&\n        //other.temperature == temperature &&\n        other.pveAddr == pveAddr &&\n        other.pveIgnoreCert == pveIgnoreCert &&\n        other.cmds == cmds &&\n        other.preferTempDev == preferTempDev &&\n        other.logoUrl == logoUrl &&\n        other.netDev == netDev &&\n        other.scriptDir == scriptDir;\n  }\n\n  @override\n  int get hashCode =>\n      //temperature.hashCode ^\n      pveAddr.hashCode ^\n      pveIgnoreCert.hashCode ^\n      cmds.hashCode ^\n      preferTempDev.hashCode ^\n      logoUrl.hashCode ^\n      netDev.hashCode ^\n      scriptDir.hashCode;\n}\n"
  },
  {
    "path": "lib/data/model/server/custom.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'custom.dart';\n\n// **************************************************************************\n// JsonSerializableGenerator\n// **************************************************************************\n\nServerCustom _$ServerCustomFromJson(Map<String, dynamic> json) => ServerCustom(\n  pveAddr: json['pveAddr'] as String?,\n  pveIgnoreCert: json['pveIgnoreCert'] as bool? ?? false,\n  cmds: (json['cmds'] as Map<String, dynamic>?)?.map(\n    (k, e) => MapEntry(k, e as String),\n  ),\n  preferTempDev: json['preferTempDev'] as String?,\n  logoUrl: json['logoUrl'] as String?,\n  netDev: json['netDev'] as String?,\n  scriptDir: json['scriptDir'] as String?,\n);\n\nMap<String, dynamic> _$ServerCustomToJson(ServerCustom instance) =>\n    <String, dynamic>{\n      'pveAddr': ?instance.pveAddr,\n      'pveIgnoreCert': instance.pveIgnoreCert,\n      'cmds': ?instance.cmds,\n      'preferTempDev': ?instance.preferTempDev,\n      'logoUrl': ?instance.logoUrl,\n      'netDev': ?instance.netDev,\n      'scriptDir': ?instance.scriptDir,\n    };\n"
  },
  {
    "path": "lib/data/model/server/discovery_result.dart",
    "content": "import 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'discovery_result.freezed.dart';\npart 'discovery_result.g.dart';\n\n@freezed\nabstract class SshDiscoveryResult with _$SshDiscoveryResult {\n  const factory SshDiscoveryResult({\n    required String ip,\n    required int port,\n    String? banner,\n    @Default(false) bool isSelected,\n  }) = _SshDiscoveryResult;\n\n  factory SshDiscoveryResult.fromJson(Map<String, dynamic> json) => _$SshDiscoveryResultFromJson(json);\n}\n\n@freezed\nabstract class SshDiscoveryReport with _$SshDiscoveryReport {\n  const factory SshDiscoveryReport({\n    required String generatedAt,\n    required int durationMs,\n    required int count,\n    required List<SshDiscoveryResult> items,\n  }) = _SshDiscoveryReport;\n\n  factory SshDiscoveryReport.fromJson(Map<String, dynamic> json) => _$SshDiscoveryReportFromJson(json);\n}\n\n@freezed\nabstract class SshDiscoveryConfig with _$SshDiscoveryConfig {\n  const factory SshDiscoveryConfig({\n    @Default(700) int timeoutMs,\n    @Default(128) int maxConcurrency,\n    @Default(false) bool enableMdns,\n    @Default(4096) int hostEnumerationLimit,\n  }) = _SshDiscoveryConfig;\n}\n\nextension SshDiscoveryConfigX on SshDiscoveryConfig {\n  List<String> toArgs() {\n    final args = <String>[];\n    args.add('--timeout-ms=$timeoutMs');\n    args.add('--max-concurrency=$maxConcurrency');\n    args.add('--host-enumeration-limit=$hostEnumerationLimit');\n    if (enableMdns) args.add('--enable-mdns');\n    return args;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/discovery_result.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'discovery_result.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n\n/// @nodoc\nmixin _$SshDiscoveryResult {\n\n String get ip; int get port; String? get banner; bool get isSelected;\n/// Create a copy of SshDiscoveryResult\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$SshDiscoveryResultCopyWith<SshDiscoveryResult> get copyWith => _$SshDiscoveryResultCopyWithImpl<SshDiscoveryResult>(this as SshDiscoveryResult, _$identity);\n\n  /// Serializes this SshDiscoveryResult to a JSON map.\n  Map<String, dynamic> toJson();\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is SshDiscoveryResult&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.banner, banner) || other.banner == banner)&&(identical(other.isSelected, isSelected) || other.isSelected == isSelected));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,ip,port,banner,isSelected);\n\n@override\nString toString() {\n  return 'SshDiscoveryResult(ip: $ip, port: $port, banner: $banner, isSelected: $isSelected)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $SshDiscoveryResultCopyWith<$Res>  {\n  factory $SshDiscoveryResultCopyWith(SshDiscoveryResult value, $Res Function(SshDiscoveryResult) _then) = _$SshDiscoveryResultCopyWithImpl;\n@useResult\n$Res call({\n String ip, int port, String? banner, bool isSelected\n});\n\n\n\n\n}\n/// @nodoc\nclass _$SshDiscoveryResultCopyWithImpl<$Res>\n    implements $SshDiscoveryResultCopyWith<$Res> {\n  _$SshDiscoveryResultCopyWithImpl(this._self, this._then);\n\n  final SshDiscoveryResult _self;\n  final $Res Function(SshDiscoveryResult) _then;\n\n/// Create a copy of SshDiscoveryResult\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? ip = null,Object? port = null,Object? banner = freezed,Object? isSelected = null,}) {\n  return _then(_self.copyWith(\nip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable\nas String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable\nas int,banner: freezed == banner ? _self.banner : banner // ignore: cast_nullable_to_non_nullable\nas String?,isSelected: null == isSelected ? _self.isSelected : isSelected // ignore: cast_nullable_to_non_nullable\nas bool,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [SshDiscoveryResult].\nextension SshDiscoveryResultPatterns on SshDiscoveryResult {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SshDiscoveryResult value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _SshDiscoveryResult() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SshDiscoveryResult value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SshDiscoveryResult():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SshDiscoveryResult value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SshDiscoveryResult() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String ip,  int port,  String? banner,  bool isSelected)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _SshDiscoveryResult() when $default != null:\nreturn $default(_that.ip,_that.port,_that.banner,_that.isSelected);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String ip,  int port,  String? banner,  bool isSelected)  $default,) {final _that = this;\nswitch (_that) {\ncase _SshDiscoveryResult():\nreturn $default(_that.ip,_that.port,_that.banner,_that.isSelected);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String ip,  int port,  String? banner,  bool isSelected)?  $default,) {final _that = this;\nswitch (_that) {\ncase _SshDiscoveryResult() when $default != null:\nreturn $default(_that.ip,_that.port,_that.banner,_that.isSelected);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n@JsonSerializable()\n\nclass _SshDiscoveryResult implements SshDiscoveryResult {\n  const _SshDiscoveryResult({required this.ip, required this.port, this.banner, this.isSelected = false});\n  factory _SshDiscoveryResult.fromJson(Map<String, dynamic> json) => _$SshDiscoveryResultFromJson(json);\n\n@override final  String ip;\n@override final  int port;\n@override final  String? banner;\n@override@JsonKey() final  bool isSelected;\n\n/// Create a copy of SshDiscoveryResult\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$SshDiscoveryResultCopyWith<_SshDiscoveryResult> get copyWith => __$SshDiscoveryResultCopyWithImpl<_SshDiscoveryResult>(this, _$identity);\n\n@override\nMap<String, dynamic> toJson() {\n  return _$SshDiscoveryResultToJson(this, );\n}\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SshDiscoveryResult&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.banner, banner) || other.banner == banner)&&(identical(other.isSelected, isSelected) || other.isSelected == isSelected));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,ip,port,banner,isSelected);\n\n@override\nString toString() {\n  return 'SshDiscoveryResult(ip: $ip, port: $port, banner: $banner, isSelected: $isSelected)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$SshDiscoveryResultCopyWith<$Res> implements $SshDiscoveryResultCopyWith<$Res> {\n  factory _$SshDiscoveryResultCopyWith(_SshDiscoveryResult value, $Res Function(_SshDiscoveryResult) _then) = __$SshDiscoveryResultCopyWithImpl;\n@override @useResult\n$Res call({\n String ip, int port, String? banner, bool isSelected\n});\n\n\n\n\n}\n/// @nodoc\nclass __$SshDiscoveryResultCopyWithImpl<$Res>\n    implements _$SshDiscoveryResultCopyWith<$Res> {\n  __$SshDiscoveryResultCopyWithImpl(this._self, this._then);\n\n  final _SshDiscoveryResult _self;\n  final $Res Function(_SshDiscoveryResult) _then;\n\n/// Create a copy of SshDiscoveryResult\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? ip = null,Object? port = null,Object? banner = freezed,Object? isSelected = null,}) {\n  return _then(_SshDiscoveryResult(\nip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable\nas String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable\nas int,banner: freezed == banner ? _self.banner : banner // ignore: cast_nullable_to_non_nullable\nas String?,isSelected: null == isSelected ? _self.isSelected : isSelected // ignore: cast_nullable_to_non_nullable\nas bool,\n  ));\n}\n\n\n}\n\n\n/// @nodoc\nmixin _$SshDiscoveryReport {\n\n String get generatedAt; int get durationMs; int get count; List<SshDiscoveryResult> get items;\n/// Create a copy of SshDiscoveryReport\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$SshDiscoveryReportCopyWith<SshDiscoveryReport> get copyWith => _$SshDiscoveryReportCopyWithImpl<SshDiscoveryReport>(this as SshDiscoveryReport, _$identity);\n\n  /// Serializes this SshDiscoveryReport to a JSON map.\n  Map<String, dynamic> toJson();\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is SshDiscoveryReport&&(identical(other.generatedAt, generatedAt) || other.generatedAt == generatedAt)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs)&&(identical(other.count, count) || other.count == count)&&const DeepCollectionEquality().equals(other.items, items));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,generatedAt,durationMs,count,const DeepCollectionEquality().hash(items));\n\n@override\nString toString() {\n  return 'SshDiscoveryReport(generatedAt: $generatedAt, durationMs: $durationMs, count: $count, items: $items)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $SshDiscoveryReportCopyWith<$Res>  {\n  factory $SshDiscoveryReportCopyWith(SshDiscoveryReport value, $Res Function(SshDiscoveryReport) _then) = _$SshDiscoveryReportCopyWithImpl;\n@useResult\n$Res call({\n String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items\n});\n\n\n\n\n}\n/// @nodoc\nclass _$SshDiscoveryReportCopyWithImpl<$Res>\n    implements $SshDiscoveryReportCopyWith<$Res> {\n  _$SshDiscoveryReportCopyWithImpl(this._self, this._then);\n\n  final SshDiscoveryReport _self;\n  final $Res Function(SshDiscoveryReport) _then;\n\n/// Create a copy of SshDiscoveryReport\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? generatedAt = null,Object? durationMs = null,Object? count = null,Object? items = null,}) {\n  return _then(_self.copyWith(\ngeneratedAt: null == generatedAt ? _self.generatedAt : generatedAt // ignore: cast_nullable_to_non_nullable\nas String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable\nas int,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable\nas int,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable\nas List<SshDiscoveryResult>,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [SshDiscoveryReport].\nextension SshDiscoveryReportPatterns on SshDiscoveryReport {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SshDiscoveryReport value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _SshDiscoveryReport() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SshDiscoveryReport value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SshDiscoveryReport():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SshDiscoveryReport value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SshDiscoveryReport() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String generatedAt,  int durationMs,  int count,  List<SshDiscoveryResult> items)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _SshDiscoveryReport() when $default != null:\nreturn $default(_that.generatedAt,_that.durationMs,_that.count,_that.items);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String generatedAt,  int durationMs,  int count,  List<SshDiscoveryResult> items)  $default,) {final _that = this;\nswitch (_that) {\ncase _SshDiscoveryReport():\nreturn $default(_that.generatedAt,_that.durationMs,_that.count,_that.items);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String generatedAt,  int durationMs,  int count,  List<SshDiscoveryResult> items)?  $default,) {final _that = this;\nswitch (_that) {\ncase _SshDiscoveryReport() when $default != null:\nreturn $default(_that.generatedAt,_that.durationMs,_that.count,_that.items);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n@JsonSerializable()\n\nclass _SshDiscoveryReport implements SshDiscoveryReport {\n  const _SshDiscoveryReport({required this.generatedAt, required this.durationMs, required this.count, required final  List<SshDiscoveryResult> items}): _items = items;\n  factory _SshDiscoveryReport.fromJson(Map<String, dynamic> json) => _$SshDiscoveryReportFromJson(json);\n\n@override final  String generatedAt;\n@override final  int durationMs;\n@override final  int count;\n final  List<SshDiscoveryResult> _items;\n@override List<SshDiscoveryResult> get items {\n  if (_items is EqualUnmodifiableListView) return _items;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(_items);\n}\n\n\n/// Create a copy of SshDiscoveryReport\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$SshDiscoveryReportCopyWith<_SshDiscoveryReport> get copyWith => __$SshDiscoveryReportCopyWithImpl<_SshDiscoveryReport>(this, _$identity);\n\n@override\nMap<String, dynamic> toJson() {\n  return _$SshDiscoveryReportToJson(this, );\n}\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SshDiscoveryReport&&(identical(other.generatedAt, generatedAt) || other.generatedAt == generatedAt)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs)&&(identical(other.count, count) || other.count == count)&&const DeepCollectionEquality().equals(other._items, _items));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,generatedAt,durationMs,count,const DeepCollectionEquality().hash(_items));\n\n@override\nString toString() {\n  return 'SshDiscoveryReport(generatedAt: $generatedAt, durationMs: $durationMs, count: $count, items: $items)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$SshDiscoveryReportCopyWith<$Res> implements $SshDiscoveryReportCopyWith<$Res> {\n  factory _$SshDiscoveryReportCopyWith(_SshDiscoveryReport value, $Res Function(_SshDiscoveryReport) _then) = __$SshDiscoveryReportCopyWithImpl;\n@override @useResult\n$Res call({\n String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items\n});\n\n\n\n\n}\n/// @nodoc\nclass __$SshDiscoveryReportCopyWithImpl<$Res>\n    implements _$SshDiscoveryReportCopyWith<$Res> {\n  __$SshDiscoveryReportCopyWithImpl(this._self, this._then);\n\n  final _SshDiscoveryReport _self;\n  final $Res Function(_SshDiscoveryReport) _then;\n\n/// Create a copy of SshDiscoveryReport\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? generatedAt = null,Object? durationMs = null,Object? count = null,Object? items = null,}) {\n  return _then(_SshDiscoveryReport(\ngeneratedAt: null == generatedAt ? _self.generatedAt : generatedAt // ignore: cast_nullable_to_non_nullable\nas String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable\nas int,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable\nas int,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable\nas List<SshDiscoveryResult>,\n  ));\n}\n\n\n}\n\n/// @nodoc\nmixin _$SshDiscoveryConfig {\n\n int get timeoutMs; int get maxConcurrency; bool get enableMdns; int get hostEnumerationLimit;\n/// Create a copy of SshDiscoveryConfig\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$SshDiscoveryConfigCopyWith<SshDiscoveryConfig> get copyWith => _$SshDiscoveryConfigCopyWithImpl<SshDiscoveryConfig>(this as SshDiscoveryConfig, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is SshDiscoveryConfig&&(identical(other.timeoutMs, timeoutMs) || other.timeoutMs == timeoutMs)&&(identical(other.maxConcurrency, maxConcurrency) || other.maxConcurrency == maxConcurrency)&&(identical(other.enableMdns, enableMdns) || other.enableMdns == enableMdns)&&(identical(other.hostEnumerationLimit, hostEnumerationLimit) || other.hostEnumerationLimit == hostEnumerationLimit));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,timeoutMs,maxConcurrency,enableMdns,hostEnumerationLimit);\n\n@override\nString toString() {\n  return 'SshDiscoveryConfig(timeoutMs: $timeoutMs, maxConcurrency: $maxConcurrency, enableMdns: $enableMdns, hostEnumerationLimit: $hostEnumerationLimit)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $SshDiscoveryConfigCopyWith<$Res>  {\n  factory $SshDiscoveryConfigCopyWith(SshDiscoveryConfig value, $Res Function(SshDiscoveryConfig) _then) = _$SshDiscoveryConfigCopyWithImpl;\n@useResult\n$Res call({\n int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit\n});\n\n\n\n\n}\n/// @nodoc\nclass _$SshDiscoveryConfigCopyWithImpl<$Res>\n    implements $SshDiscoveryConfigCopyWith<$Res> {\n  _$SshDiscoveryConfigCopyWithImpl(this._self, this._then);\n\n  final SshDiscoveryConfig _self;\n  final $Res Function(SshDiscoveryConfig) _then;\n\n/// Create a copy of SshDiscoveryConfig\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? timeoutMs = null,Object? maxConcurrency = null,Object? enableMdns = null,Object? hostEnumerationLimit = null,}) {\n  return _then(_self.copyWith(\ntimeoutMs: null == timeoutMs ? _self.timeoutMs : timeoutMs // ignore: cast_nullable_to_non_nullable\nas int,maxConcurrency: null == maxConcurrency ? _self.maxConcurrency : maxConcurrency // ignore: cast_nullable_to_non_nullable\nas int,enableMdns: null == enableMdns ? _self.enableMdns : enableMdns // ignore: cast_nullable_to_non_nullable\nas bool,hostEnumerationLimit: null == hostEnumerationLimit ? _self.hostEnumerationLimit : hostEnumerationLimit // ignore: cast_nullable_to_non_nullable\nas int,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [SshDiscoveryConfig].\nextension SshDiscoveryConfigPatterns on SshDiscoveryConfig {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SshDiscoveryConfig value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _SshDiscoveryConfig() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SshDiscoveryConfig value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SshDiscoveryConfig():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SshDiscoveryConfig value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SshDiscoveryConfig() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int timeoutMs,  int maxConcurrency,  bool enableMdns,  int hostEnumerationLimit)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _SshDiscoveryConfig() when $default != null:\nreturn $default(_that.timeoutMs,_that.maxConcurrency,_that.enableMdns,_that.hostEnumerationLimit);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int timeoutMs,  int maxConcurrency,  bool enableMdns,  int hostEnumerationLimit)  $default,) {final _that = this;\nswitch (_that) {\ncase _SshDiscoveryConfig():\nreturn $default(_that.timeoutMs,_that.maxConcurrency,_that.enableMdns,_that.hostEnumerationLimit);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int timeoutMs,  int maxConcurrency,  bool enableMdns,  int hostEnumerationLimit)?  $default,) {final _that = this;\nswitch (_that) {\ncase _SshDiscoveryConfig() when $default != null:\nreturn $default(_that.timeoutMs,_that.maxConcurrency,_that.enableMdns,_that.hostEnumerationLimit);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n\nclass _SshDiscoveryConfig implements SshDiscoveryConfig {\n  const _SshDiscoveryConfig({this.timeoutMs = 700, this.maxConcurrency = 128, this.enableMdns = false, this.hostEnumerationLimit = 4096});\n  \n\n@override@JsonKey() final  int timeoutMs;\n@override@JsonKey() final  int maxConcurrency;\n@override@JsonKey() final  bool enableMdns;\n@override@JsonKey() final  int hostEnumerationLimit;\n\n/// Create a copy of SshDiscoveryConfig\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$SshDiscoveryConfigCopyWith<_SshDiscoveryConfig> get copyWith => __$SshDiscoveryConfigCopyWithImpl<_SshDiscoveryConfig>(this, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SshDiscoveryConfig&&(identical(other.timeoutMs, timeoutMs) || other.timeoutMs == timeoutMs)&&(identical(other.maxConcurrency, maxConcurrency) || other.maxConcurrency == maxConcurrency)&&(identical(other.enableMdns, enableMdns) || other.enableMdns == enableMdns)&&(identical(other.hostEnumerationLimit, hostEnumerationLimit) || other.hostEnumerationLimit == hostEnumerationLimit));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,timeoutMs,maxConcurrency,enableMdns,hostEnumerationLimit);\n\n@override\nString toString() {\n  return 'SshDiscoveryConfig(timeoutMs: $timeoutMs, maxConcurrency: $maxConcurrency, enableMdns: $enableMdns, hostEnumerationLimit: $hostEnumerationLimit)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$SshDiscoveryConfigCopyWith<$Res> implements $SshDiscoveryConfigCopyWith<$Res> {\n  factory _$SshDiscoveryConfigCopyWith(_SshDiscoveryConfig value, $Res Function(_SshDiscoveryConfig) _then) = __$SshDiscoveryConfigCopyWithImpl;\n@override @useResult\n$Res call({\n int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit\n});\n\n\n\n\n}\n/// @nodoc\nclass __$SshDiscoveryConfigCopyWithImpl<$Res>\n    implements _$SshDiscoveryConfigCopyWith<$Res> {\n  __$SshDiscoveryConfigCopyWithImpl(this._self, this._then);\n\n  final _SshDiscoveryConfig _self;\n  final $Res Function(_SshDiscoveryConfig) _then;\n\n/// Create a copy of SshDiscoveryConfig\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? timeoutMs = null,Object? maxConcurrency = null,Object? enableMdns = null,Object? hostEnumerationLimit = null,}) {\n  return _then(_SshDiscoveryConfig(\ntimeoutMs: null == timeoutMs ? _self.timeoutMs : timeoutMs // ignore: cast_nullable_to_non_nullable\nas int,maxConcurrency: null == maxConcurrency ? _self.maxConcurrency : maxConcurrency // ignore: cast_nullable_to_non_nullable\nas int,enableMdns: null == enableMdns ? _self.enableMdns : enableMdns // ignore: cast_nullable_to_non_nullable\nas bool,hostEnumerationLimit: null == hostEnumerationLimit ? _self.hostEnumerationLimit : hostEnumerationLimit // ignore: cast_nullable_to_non_nullable\nas int,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/model/server/discovery_result.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'discovery_result.dart';\n\n// **************************************************************************\n// JsonSerializableGenerator\n// **************************************************************************\n\n_SshDiscoveryResult _$SshDiscoveryResultFromJson(Map<String, dynamic> json) =>\n    _SshDiscoveryResult(\n      ip: json['ip'] as String,\n      port: (json['port'] as num).toInt(),\n      banner: json['banner'] as String?,\n      isSelected: json['isSelected'] as bool? ?? false,\n    );\n\nMap<String, dynamic> _$SshDiscoveryResultToJson(_SshDiscoveryResult instance) =>\n    <String, dynamic>{\n      'ip': instance.ip,\n      'port': instance.port,\n      'banner': instance.banner,\n      'isSelected': instance.isSelected,\n    };\n\n_SshDiscoveryReport _$SshDiscoveryReportFromJson(Map<String, dynamic> json) =>\n    _SshDiscoveryReport(\n      generatedAt: json['generatedAt'] as String,\n      durationMs: (json['durationMs'] as num).toInt(),\n      count: (json['count'] as num).toInt(),\n      items: (json['items'] as List<dynamic>)\n          .map((e) => SshDiscoveryResult.fromJson(e as Map<String, dynamic>))\n          .toList(),\n    );\n\nMap<String, dynamic> _$SshDiscoveryReportToJson(_SshDiscoveryReport instance) =>\n    <String, dynamic>{\n      'generatedAt': instance.generatedAt,\n      'durationMs': instance.durationMs,\n      'count': instance.count,\n      'items': instance.items,\n    };\n"
  },
  {
    "path": "lib/data/model/server/disk.dart",
    "content": "import 'dart:convert';\n\nimport 'package:equatable/equatable.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/server/time_seq.dart';\n\nimport 'package:server_box/data/res/misc.dart';\n\nclass Disk with EquatableMixin {\n  final String path;\n  final String? fsTyp;\n  final String mount;\n  final int usedPercent;\n  final BigInt used;\n  final BigInt size;\n  final BigInt avail;\n\n  /// Device name (e.g., sda1, nvme0n1p1)\n  final String? name;\n\n  /// Internal kernel device name\n  final String? kname;\n\n  /// Filesystem UUID\n  final String? uuid;\n\n  /// Child disks (partitions)\n  final List<Disk> children;\n\n  const Disk({\n    required this.path,\n    this.fsTyp,\n    required this.mount,\n    required this.usedPercent,\n    required this.used,\n    required this.size,\n    required this.avail,\n    this.name,\n    this.kname,\n    this.uuid,\n    this.children = const [],\n  });\n\n  static List<Disk> parse(String raw) {\n    final list = <Disk>[];\n    raw = raw.trim();\n    \n    if (raw.isEmpty) {\n      dprint('Empty disk info data received');\n      return list;\n    }\n\n    try {\n      // Check if we have lsblk JSON output with success marker\n      if (raw.startsWith('{')) {\n        // Extract JSON part (excluding the success marker if present)\n        final jsonEnd = raw.indexOf('\\nLSBLK_SUCCESS');\n        final jsonPart = jsonEnd > 0 ? raw.substring(0, jsonEnd) : raw;\n        \n        try {\n          final Map<String, dynamic> jsonData = json.decode(jsonPart);\n          final List<dynamic> blockdevices = jsonData['blockdevices'] ?? [];\n\n          for (final device in blockdevices) {\n            // Process each device\n            _processTopLevelDevice(device, list);\n          }\n          \n          // If we successfully parsed JSON and have valid disks, return them\n          if (list.isNotEmpty) {\n            return list;\n          }\n        } on FormatException catch (e) {\n          Loggers.app.warning('JSON parsing failed, falling back to df -k output: $e');\n        } catch (e) {\n          Loggers.app.warning('Error processing JSON disk data, falling back to df -k output: $e', e);\n        }\n      }\n      \n      // Check if we have df -k output (fallback case)\n      if (raw.contains('Filesystem') && raw.contains('Mounted on')) {\n        return _parseWithOldMethod(raw);\n      }\n      \n      // If we reach here, both parsing methods failed\n      Loggers.app.warning('Unable to parse disk info with any method');\n      \n    } catch (e) {\n      Loggers.app.warning('Failed to parse disk info with both methods: $e', e);\n    }\n    return list;\n  }\n\n  /// Process a top-level device and add all valid disks to the list\n  static void _processTopLevelDevice(Map<String, dynamic> device, List<Disk> list) {\n    final disk = _processDiskDevice(device);\n    if (disk != null) {\n      list.add(disk);\n    }\n\n    // For devices with children (like physical disks with partitions),\n    // also process each child individually to ensure BTRFS RAID disks are properly handled\n    final List<dynamic> childDevices = device['children'] ?? [];\n    for (final childDevice in childDevices) {\n      final String childPath = childDevice['path']?.toString() ?? '';\n      final String childFsType = childDevice['fstype']?.toString() ?? '';\n\n      // If this is a BTRFS partition, add it directly to ensure it's properly represented\n      if (childFsType == 'btrfs' && childPath.isNotEmpty) {\n        final childDisk = _processSingleDevice(childDevice);\n        if (childDisk != null) {\n          list.add(childDisk);\n        }\n      }\n    }\n  }\n\n  /// Parse filesystem fields from device data\n  static ({BigInt size, BigInt used, BigInt avail, int usedPercent}) _parseFilesystemFields(Map<String, dynamic> device) {\n    // Helper function to parse size strings safely\n    BigInt parseSize(String? sizeStr) {\n      if (sizeStr == null || sizeStr.isEmpty || sizeStr == 'null' || sizeStr == '0') {\n        return BigInt.zero;\n      }\n      return (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);\n    }\n\n    // Helper function to parse percentage strings\n    int parsePercent(String? percentStr) {\n      if (percentStr == null || percentStr.isEmpty || percentStr == 'null') {\n        return 0;\n      }\n      return int.tryParse(percentStr.replaceAll('%', '')) ?? 0;\n    }\n\n    return (\n      size: parseSize(device['fssize']?.toString()),\n      used: parseSize(device['fsused']?.toString()),\n      avail: parseSize(device['fsavail']?.toString()),\n      usedPercent: parsePercent(device['fsuse%']?.toString()),\n    );\n  }\n\n  /// Process a single device without recursively processing its children\n  static Disk? _processSingleDevice(Map<String, dynamic> device) {\n    final fstype = device['fstype']?.toString();\n    final String mountpoint = device['mountpoint']?.toString() ?? '';\n    final String path = device['path']?.toString() ?? '';\n\n    if (path.isEmpty || (fstype == null && mountpoint.isEmpty)) {\n      return null;\n    }\n\n    if (!_shouldCalc(fstype ?? '', mountpoint)) {\n      return null;\n    }\n\n    final fsFields = _parseFilesystemFields(device);\n    final name = device['name']?.toString();\n    final kname = device['kname']?.toString();\n    final uuid = device['uuid']?.toString();\n\n    return Disk(\n      path: path,\n      fsTyp: fstype,\n      mount: mountpoint,\n      usedPercent: fsFields.usedPercent,\n      used: fsFields.used,\n      size: fsFields.size,\n      avail: fsFields.avail,\n      name: name,\n      kname: kname,\n      uuid: uuid,\n      children: const [], // No children for direct device\n    );\n  }\n\n  static Disk? _processDiskDevice(Map<String, dynamic> device) {\n    final fstype = device['fstype']?.toString();\n    final String mountpoint = device['mountpoint']?.toString() ?? '';\n\n    // For parent devices that don't have a mountpoint themselves\n    final String path = device['path']?.toString() ?? '';\n    final String mount = mountpoint;\n    final List<Disk> childDisks = [];\n\n    // Process children devices recursively\n    final List<dynamic> childDevices = device['children'] ?? [];\n    for (final childDevice in childDevices) {\n      final childDisk = _processDiskDevice(childDevice);\n      if (childDisk != null) {\n        childDisks.add(childDisk);\n      }\n    }\n\n    // Handle common filesystem cases or parent devices with children\n    if ((fstype != null && _shouldCalc(fstype, mount)) || (childDisks.isNotEmpty && path.isNotEmpty)) {\n      final fsFields = _parseFilesystemFields(device);\n      final name = device['name']?.toString();\n      final kname = device['kname']?.toString();\n      final uuid = device['uuid']?.toString();\n\n      return Disk(\n        path: path,\n        fsTyp: fstype,\n        mount: mount,\n        usedPercent: fsFields.usedPercent,\n        used: fsFields.used,\n        size: fsFields.size,\n        avail: fsFields.avail,\n        name: name,\n        kname: kname,\n        uuid: uuid,\n        children: childDisks,\n      );\n    } else if (childDisks.isNotEmpty) {\n      // If this is a parent device with no filesystem but has children,\n      // return the first valid child instead\n      if (childDisks.isNotEmpty) {\n        return childDisks.first;\n      }\n    }\n\n    return null;\n  }\n\n  // Fallback to the old parsing method in case JSON parsing fails\n  static List<Disk> _parseWithOldMethod(String raw) {\n    final list = <Disk>[];\n    final items = raw.split('\\n');\n    if (items.isNotEmpty) items.removeAt(0);\n    var pathCache = '';\n    for (var item in items) {\n      if (item.isEmpty) {\n        continue;\n      }\n      final vals = item.split(Miscs.blankReg);\n      if (vals.length == 1) {\n        pathCache = vals[0];\n        continue;\n      }\n      if (pathCache != '') {\n        vals[0] = pathCache;\n        pathCache = '';\n      }\n      try {\n        final fs = vals[0];\n        final mount = vals[5];\n        if (!_shouldCalc(fs, mount)) continue;\n        list.add(\n          Disk(\n            path: fs,\n            mount: mount,\n            usedPercent: int.parse(vals[4].replaceFirst('%', '')),\n            used: BigInt.parse(vals[2]) ~/ BigInt.from(1024),\n            size: BigInt.parse(vals[1]) ~/ BigInt.from(1024),\n            avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024),\n          ),\n        );\n      } catch (e) {\n        continue;\n      }\n    }\n    return list;\n  }\n\n  @override\n  List<Object?> get props => [\n    path,\n    name,\n    kname,\n    fsTyp,\n    mount,\n    usedPercent,\n    used,\n    size,\n    avail,\n    uuid,\n    children,\n  ];\n}\n\nclass DiskIO extends TimeSeq<DiskIOPiece> {\n  DiskIO(super.init1, super.init2);\n\n  @override\n  void onUpdate() {\n    cachedAllSpeed = _getAllSpeed();\n  }\n\n  (double?, double?) _getSpeed(String dev) {\n    // Extract the device name from path if needed\n    String searchDev = dev;\n    if (dev.startsWith('/dev/')) {\n      searchDev = dev.substring(5);\n    }\n\n    // Try to find by exact device name first\n    final old = pre.firstWhereOrNull((e) => e.dev == searchDev);\n    final new_ = now.firstWhereOrNull((e) => e.dev == searchDev);\n\n    if (old == null || new_ == null) return (null, null);\n    final sectorsRead = new_.sectorsRead - old.sectorsRead;\n    final sectorsWrite = new_.sectorsWrite - old.sectorsWrite;\n    final time = new_.time - old.time;\n    final read = sectorsRead / time * 512;\n    final write = sectorsWrite / time * 512;\n    return (read, write);\n  }\n\n  (String?, String?) getSpeed(String dev) {\n    final (read_, write_) = _getSpeed(dev);\n    if (read_ == null || write_ == null) return (null, null);\n    final read = '${read_.bytes2Str}/s';\n    final write = '${write_.bytes2Str}/s';\n    return (read, write);\n  }\n\n  (String?, String?) cachedAllSpeed = (null, null);\n  (String?, String?) _getAllSpeed() {\n    if (pre.isEmpty || now.isEmpty) return (null, null);\n    var (read, write) = (0.0, 0.0);\n    for (var item in pre) {\n      /// Issue #314\n      /// Only calc nvme, sd, vd, hd, mmcblk, sr\n      if (!item.dev.startsWith('nvme') &&\n          !item.dev.startsWith('sd') &&\n          !item.dev.startsWith('vd') &&\n          !item.dev.startsWith('hd') &&\n          !item.dev.startsWith('mmcblk') &&\n          !item.dev.startsWith('sr')) {\n        continue;\n      }\n      final (read_, write_) = _getSpeed(item.dev);\n      read += read_ ?? 0;\n      write += write_ ?? 0;\n    }\n\n    final readStr = '${read.bytes2Str}/s';\n    final writeStr = '${write.bytes2Str}/s';\n    return (readStr, writeStr);\n  }\n\n  static List<DiskIOPiece> parse(String raw, int time) {\n    final lines = raw.split('\\n');\n    if (lines.isEmpty) return [];\n    final items = <DiskIOPiece>[];\n    for (var item in lines) {\n      item = item.trim();\n      if (item.isEmpty) continue;\n      final vals = item.split(Miscs.blankReg);\n      if (vals.length < 10) continue;\n      try {\n        final dev = vals[2];\n        if (dev.startsWith('loop')) continue;\n        items.add(\n          DiskIOPiece(\n            dev: dev,\n            sectorsRead: int.parse(vals[5]),\n            sectorsWrite: int.parse(vals[9]),\n            time: time,\n          ),\n        );\n      } catch (e) {\n        continue;\n      }\n    }\n    return items;\n  }\n}\n\nclass DiskIOPiece extends TimeSeqIface<DiskIOPiece> {\n  final String dev;\n  final int sectorsRead;\n  final int sectorsWrite;\n  final int time;\n\n  DiskIOPiece({required this.dev, required this.sectorsRead, required this.sectorsWrite, required this.time});\n\n  @override\n  bool same(DiskIOPiece other) => dev == other.dev;\n}\n\nclass DiskUsage {\n  final BigInt used;\n  final BigInt size;\n\n  DiskUsage({required this.used, required this.size});\n\n  double get usedPercent {\n    // Avoid division by zero\n    if (size == BigInt.zero) return 0;\n    return used / size * 100;\n  }\n\n  /// Find all devs, add their used and size\n  static DiskUsage parse(List<Disk> disks) {\n    final devs = <String>{};\n    var used = BigInt.zero;\n    var size = BigInt.zero;\n    for (var disk in disks) {\n      if (!_shouldCalc(disk.path, disk.mount)) continue;\n      // Use a combination of path and kernel name to uniquely identify disks\n      // This helps distinguish between multiple physical disks in BTRFS RAID setups\n      final uniqueId = '${disk.path}:${disk.kname ?? \"unknown\"}';\n      if (devs.contains(uniqueId)) continue;\n      devs.add(uniqueId);\n      used += disk.used;\n      size += disk.size;\n    }\n    return DiskUsage(used: used, size: size);\n  }\n}\n\nbool _shouldCalc(String fs, String mount) {\n  // Skip swap partitions\n  // if (mount == '[SWAP]') return false;\n\n  // Include standard filesystems\n  if (fs.startsWith('/dev')) return true;\n  // Some NAS may have mounted path like this `//192.168.1.2/`\n  if (fs.startsWith('//')) return true;\n  if (mount.startsWith('/mnt')) return true;\n\n  // Include common filesystem types\n  // final commonFsTypes = ['ext2', 'ext3', 'ext4', 'xfs', 'btrfs', 'zfs', 'ntfs', 'fat', 'vfat'];\n  // if (commonFsTypes.any((type) => fs.toLowerCase() == type)) return true;\n\n  // Skip special filesystems\n  // if (fs == 'LVM2_member' || fs == 'crypto_LUKS') return false;\n  if (fs.startsWith('shm') || fs.startsWith('overlay') || fs.startsWith('tmpfs')) {\n    return false;\n  }\n\n  return true;\n}\n"
  },
  {
    "path": "lib/data/model/server/disk_smart.dart",
    "content": "import 'dart:convert';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'disk_smart.freezed.dart';\npart 'disk_smart.g.dart';\n\n@freezed\nabstract class DiskSmart with _$DiskSmart {\n  const DiskSmart._();\n\n  const factory DiskSmart({\n    required String device,\n    bool? healthy,\n    double? temperature,\n    String? model,\n    String? serial,\n    int? powerOnHours,\n    int? powerCycleCount,\n    required Map<String, dynamic> rawData,\n    required Map<String, SmartAttribute> smartAttributes,\n  }) = _DiskSmart;\n\n  factory DiskSmart.fromJson(Map<String, dynamic> json) => _$DiskSmartFromJson(json);\n\n  static List<DiskSmart> parse(String raw) {\n    final results = <DiskSmart>[];\n\n    final jsonBlocks = raw.split('\\n\\n').where((s) => s.trim().isNotEmpty);\n\n    for (final jsonStr in jsonBlocks) {\n      try {\n        final data = json.decode(jsonStr.trim()) as Map<String, dynamic>;\n\n        // Basic\n        final device = data['device']?['name']?.toString() ?? '';\n\n        if (!_isPhysicalDisk(device)) continue;\n\n        final healthy = _parseHealthStatus(data);\n\n        // Model and Serial\n        final model =\n            data['model_name']?.toString() ??\n            data['model_family']?.toString() ??\n            data['device']?['model_name']?.toString();\n        final serial = data['serial_number']?.toString() ?? data['device']?['serial_number']?.toString();\n\n        // SMART Attrs\n        final smartAttributes = _parseSmartAttributes(data);\n        final temperature = _extractTemperature(data, smartAttributes);\n        final powerOnHours =\n            data['power_on_time']?['hours'] as int? ?? smartAttributes['Power_On_Hours']?.rawValue as int?;\n        final powerCycleCount =\n            data['power_cycle_count'] as int? ?? smartAttributes['Power_Cycle_Count']?.rawValue as int?;\n\n        results.add(\n          DiskSmart(\n            device: device,\n            healthy: healthy,\n            temperature: temperature,\n            model: model,\n            serial: serial,\n            powerOnHours: powerOnHours,\n            powerCycleCount: powerCycleCount,\n            rawData: data,\n            smartAttributes: smartAttributes,\n          ),\n        );\n      } catch (e, s) {\n        Loggers.app.warning('DiskSmart parse', e, s);\n      }\n    }\n    return results;\n  }\n\n  static bool _isPhysicalDisk(String device) {\n    if (device.isEmpty) return false;\n\n    // Common patterns for physical disks\n    final patterns = [\n      RegExp(r'^/dev/sd[a-z]$'), // SATA/SCSI: /dev/sda, /dev/sdb\n      RegExp(r'^/dev/hd[a-z]$'), // IDE: /dev/hda, /dev/hdb\n      RegExp(r'^/dev/nvme\\d+n\\d+$'), // NVMe: /dev/nvme0n1, /dev/nvme1n1\n      RegExp(r'^/dev/mmcblk\\d+$'), // MMC: /dev/mmcblk0\n      RegExp(r'^/dev/vd[a-z]$'), // VirtIO: /dev/vda, /dev/vdb\n      RegExp(r'^/dev/xvd[a-z]$'), // Xen: /dev/xvda, /dev/xvdb\n    ];\n\n    return patterns.any((pattern) => pattern.hasMatch(device));\n  }\n\n  static bool? _parseHealthStatus(Map<String, dynamic> data) {\n    // smart_status.passed\n    final smartStatus = data['smart_status'];\n    if (smartStatus is Map<String, dynamic>) {\n      final passed = smartStatus['passed'];\n      if (passed is bool) return passed;\n    }\n\n    // smart_status.status\n    if (smartStatus is Map<String, dynamic>) {\n      final status = smartStatus['status']?.toString().toLowerCase();\n      if (status != null) {\n        if (status.contains('pass') || status.contains('ok')) return true;\n        if (status.contains('fail')) return false;\n      }\n    }\n\n    // smart_status\n    final rootSmartStatus = data['smart_status']?.toString().toLowerCase();\n    if (rootSmartStatus != null) {\n      if (rootSmartStatus.contains('pass') || rootSmartStatus.contains('ok')) return true;\n      if (rootSmartStatus.contains('fail')) return false;\n    }\n\n    // health attrs\n    final attrTable = data['ata_smart_attributes']?['table'] as List?;\n    if (attrTable != null) {\n      var hasFailingAttributes = false;\n\n      for (final attr in attrTable) {\n        if (attr is Map<String, dynamic>) {\n          final whenFailed = attr['when_failed']?.toString();\n          if (whenFailed != null && whenFailed.isNotEmpty && whenFailed != 'never') {\n            hasFailingAttributes = true;\n            break;\n          }\n\n          // Whether the attribute is critical\n          final name = attr['name']?.toString();\n          final value = attr['value'] as int?;\n          final thresh = attr['thresh'] as int?;\n\n          if (name != null && value != null && thresh != null && thresh > 0) {\n            const criticalAttrs = [\n              'Reallocated_Sector_Ct',\n              'Reallocated_Event_Count',\n              'Current_Pending_Sector',\n              'Offline_Uncorrectable',\n              'UDMA_CRC_Error_Count',\n            ];\n\n            if (criticalAttrs.contains(name) && value < thresh) {\n              hasFailingAttributes = true;\n              break;\n            }\n          }\n        }\n      }\n\n      if (hasFailingAttributes) return false;\n    }\n\n    if (attrTable != null && attrTable.isNotEmpty) {\n      return true;\n    }\n\n    // Uncertain status, assume healthy\n    return true;\n  }\n\n  static Map<String, SmartAttribute> _parseSmartAttributes(Map<String, dynamic> data) {\n    final attributes = <String, SmartAttribute>{};\n\n    final attrTable = data['ata_smart_attributes']?['table'] as List?;\n    if (attrTable == null) return attributes;\n\n    for (final attr in attrTable) {\n      if (attr is Map<String, dynamic>) {\n        final name = attr['name']?.toString();\n        if (name != null) {\n          attributes[name] = SmartAttribute(\n            id: attr['id'] as int?,\n            name: name,\n            value: attr['value'] as int?,\n            worst: attr['worst'] as int?,\n            thresh: attr['thresh'] as int?,\n            whenFailed: attr['when_failed']?.toString(),\n            rawValue: attr['raw']?['value'],\n            rawString: attr['raw']?['string']?.toString(),\n            flags: SmartAttributeFlags.fromMap(attr['flags'] as Map<String, dynamic>? ?? {}),\n          );\n        }\n      }\n    }\n\n    return attributes;\n  }\n\n  static final _tempReg = RegExp(r'^(\\d+(?:\\.\\d+)?)');\n\n  /// Extract temperature from the data\n  static double? _extractTemperature(Map<String, dynamic> data, Map<String, SmartAttribute> attrs) {\n    // Directly\n    final directTemp = data['temperature']?['current'];\n    if (directTemp is num) return directTemp.toDouble();\n\n    // SMART attribute\n    final tempAttr = attrs['Temperature_Celsius'];\n    if (tempAttr != null) {\n      // \"35 (Min/Max 14/61)\"\n      final rawString = tempAttr.rawString;\n      if (rawString != null) {\n        final match = _tempReg.firstMatch(rawString);\n        if (match != null) {\n          return double.tryParse(match.group(1)!);\n        }\n      }\n\n      // Simple numeric value\n      if (tempAttr.rawValue is num && tempAttr.rawValue! < 150) {\n        return tempAttr.rawValue!.toDouble();\n      }\n    }\n\n    return null;\n  }\n\n  /// Get the specific SMART attribute by name\n  SmartAttribute? getAttribute(String name) => smartAttributes[name];\n\n  int? get ssdLifeLeft => smartAttributes['SSD_Life_Left']?.rawValue as int?;\n  int? get lifetimeWritesGiB => smartAttributes['Lifetime_Writes_GiB']?.rawValue as int?;\n  int? get lifetimeReadsGiB => smartAttributes['Lifetime_Reads_GiB']?.rawValue as int?;\n  int? get unsafeShutdownCount => smartAttributes['Unsafe_Shutdown_Count']?.rawValue as int?;\n  int? get averageEraseCount => smartAttributes['Average_Erase_Count']?.rawValue as int?;\n  int? get maxEraseCount => smartAttributes['Max_Erase_Count']?.rawValue as int?;\n\n  @override\n  String toString() => 'DiskSmart($device)';\n}\n\n@freezed\nabstract class SmartAttribute with _$SmartAttribute {\n  const SmartAttribute._();\n\n  const factory SmartAttribute({\n    int? id,\n    required String name,\n    int? value,\n    int? worst,\n    int? thresh,\n    String? whenFailed,\n    dynamic rawValue,\n    String? rawString,\n    required SmartAttributeFlags flags,\n  }) = _SmartAttribute;\n\n  factory SmartAttribute.fromJson(Map<String, dynamic> json) => _$SmartAttributeFromJson(json);\n\n  @override\n  String toString() {\n    return 'SmartAttribute(id: $id, name: $name)';\n  }\n}\n\n@freezed\nabstract class SmartAttributeFlags with _$SmartAttributeFlags {\n  const SmartAttributeFlags._();\n\n  const factory SmartAttributeFlags({\n    int? value,\n    String? string,\n    @Default(false) bool prefailure,\n    @Default(false) bool updatedOnline,\n    @Default(false) bool performance,\n    @Default(false) bool errorRate,\n    @Default(false) bool eventCount,\n    @Default(false) bool autoKeep,\n  }) = _SmartAttributeFlags;\n\n  factory SmartAttributeFlags.fromJson(Map<String, dynamic> json) => _$SmartAttributeFlagsFromJson(json);\n\n  factory SmartAttributeFlags.fromMap(Map<String, dynamic> map) {\n    return SmartAttributeFlags(\n      value: map['value'] as int?,\n      string: map['string']?.toString(),\n      prefailure: map['prefailure'] == true,\n      updatedOnline: map['updated_online'] == true,\n      performance: map['performance'] == true,\n      errorRate: map['error_rate'] == true,\n      eventCount: map['event_count'] == true,\n      autoKeep: map['auto_keep'] == true,\n    );\n  }\n\n  @override\n  String toString() {\n    return 'SmartAttributeFlags(value: $value, string: $string)';\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/disk_smart.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'disk_smart.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n\n/// @nodoc\nmixin _$DiskSmart {\n\n String get device; bool? get healthy; double? get temperature; String? get model; String? get serial; int? get powerOnHours; int? get powerCycleCount; Map<String, dynamic> get rawData; Map<String, SmartAttribute> get smartAttributes;\n/// Create a copy of DiskSmart\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$DiskSmartCopyWith<DiskSmart> get copyWith => _$DiskSmartCopyWithImpl<DiskSmart>(this as DiskSmart, _$identity);\n\n  /// Serializes this DiskSmart to a JSON map.\n  Map<String, dynamic> toJson();\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is DiskSmart&&(identical(other.device, device) || other.device == device)&&(identical(other.healthy, healthy) || other.healthy == healthy)&&(identical(other.temperature, temperature) || other.temperature == temperature)&&(identical(other.model, model) || other.model == model)&&(identical(other.serial, serial) || other.serial == serial)&&(identical(other.powerOnHours, powerOnHours) || other.powerOnHours == powerOnHours)&&(identical(other.powerCycleCount, powerCycleCount) || other.powerCycleCount == powerCycleCount)&&const DeepCollectionEquality().equals(other.rawData, rawData)&&const DeepCollectionEquality().equals(other.smartAttributes, smartAttributes));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,device,healthy,temperature,model,serial,powerOnHours,powerCycleCount,const DeepCollectionEquality().hash(rawData),const DeepCollectionEquality().hash(smartAttributes));\n\n\n\n}\n\n/// @nodoc\nabstract mixin class $DiskSmartCopyWith<$Res>  {\n  factory $DiskSmartCopyWith(DiskSmart value, $Res Function(DiskSmart) _then) = _$DiskSmartCopyWithImpl;\n@useResult\n$Res call({\n String device, bool? healthy, double? temperature, String? model, String? serial, int? powerOnHours, int? powerCycleCount, Map<String, dynamic> rawData, Map<String, SmartAttribute> smartAttributes\n});\n\n\n\n\n}\n/// @nodoc\nclass _$DiskSmartCopyWithImpl<$Res>\n    implements $DiskSmartCopyWith<$Res> {\n  _$DiskSmartCopyWithImpl(this._self, this._then);\n\n  final DiskSmart _self;\n  final $Res Function(DiskSmart) _then;\n\n/// Create a copy of DiskSmart\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? device = null,Object? healthy = freezed,Object? temperature = freezed,Object? model = freezed,Object? serial = freezed,Object? powerOnHours = freezed,Object? powerCycleCount = freezed,Object? rawData = null,Object? smartAttributes = null,}) {\n  return _then(_self.copyWith(\ndevice: null == device ? _self.device : device // ignore: cast_nullable_to_non_nullable\nas String,healthy: freezed == healthy ? _self.healthy : healthy // ignore: cast_nullable_to_non_nullable\nas bool?,temperature: freezed == temperature ? _self.temperature : temperature // ignore: cast_nullable_to_non_nullable\nas double?,model: freezed == model ? _self.model : model // ignore: cast_nullable_to_non_nullable\nas String?,serial: freezed == serial ? _self.serial : serial // ignore: cast_nullable_to_non_nullable\nas String?,powerOnHours: freezed == powerOnHours ? _self.powerOnHours : powerOnHours // ignore: cast_nullable_to_non_nullable\nas int?,powerCycleCount: freezed == powerCycleCount ? _self.powerCycleCount : powerCycleCount // ignore: cast_nullable_to_non_nullable\nas int?,rawData: null == rawData ? _self.rawData : rawData // ignore: cast_nullable_to_non_nullable\nas Map<String, dynamic>,smartAttributes: null == smartAttributes ? _self.smartAttributes : smartAttributes // ignore: cast_nullable_to_non_nullable\nas Map<String, SmartAttribute>,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [DiskSmart].\nextension DiskSmartPatterns on DiskSmart {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _DiskSmart value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _DiskSmart() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _DiskSmart value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _DiskSmart():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _DiskSmart value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _DiskSmart() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String device,  bool? healthy,  double? temperature,  String? model,  String? serial,  int? powerOnHours,  int? powerCycleCount,  Map<String, dynamic> rawData,  Map<String, SmartAttribute> smartAttributes)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _DiskSmart() when $default != null:\nreturn $default(_that.device,_that.healthy,_that.temperature,_that.model,_that.serial,_that.powerOnHours,_that.powerCycleCount,_that.rawData,_that.smartAttributes);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String device,  bool? healthy,  double? temperature,  String? model,  String? serial,  int? powerOnHours,  int? powerCycleCount,  Map<String, dynamic> rawData,  Map<String, SmartAttribute> smartAttributes)  $default,) {final _that = this;\nswitch (_that) {\ncase _DiskSmart():\nreturn $default(_that.device,_that.healthy,_that.temperature,_that.model,_that.serial,_that.powerOnHours,_that.powerCycleCount,_that.rawData,_that.smartAttributes);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String device,  bool? healthy,  double? temperature,  String? model,  String? serial,  int? powerOnHours,  int? powerCycleCount,  Map<String, dynamic> rawData,  Map<String, SmartAttribute> smartAttributes)?  $default,) {final _that = this;\nswitch (_that) {\ncase _DiskSmart() when $default != null:\nreturn $default(_that.device,_that.healthy,_that.temperature,_that.model,_that.serial,_that.powerOnHours,_that.powerCycleCount,_that.rawData,_that.smartAttributes);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n@JsonSerializable()\n\nclass _DiskSmart extends DiskSmart {\n  const _DiskSmart({required this.device, this.healthy, this.temperature, this.model, this.serial, this.powerOnHours, this.powerCycleCount, required final  Map<String, dynamic> rawData, required final  Map<String, SmartAttribute> smartAttributes}): _rawData = rawData,_smartAttributes = smartAttributes,super._();\n  factory _DiskSmart.fromJson(Map<String, dynamic> json) => _$DiskSmartFromJson(json);\n\n@override final  String device;\n@override final  bool? healthy;\n@override final  double? temperature;\n@override final  String? model;\n@override final  String? serial;\n@override final  int? powerOnHours;\n@override final  int? powerCycleCount;\n final  Map<String, dynamic> _rawData;\n@override Map<String, dynamic> get rawData {\n  if (_rawData is EqualUnmodifiableMapView) return _rawData;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableMapView(_rawData);\n}\n\n final  Map<String, SmartAttribute> _smartAttributes;\n@override Map<String, SmartAttribute> get smartAttributes {\n  if (_smartAttributes is EqualUnmodifiableMapView) return _smartAttributes;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableMapView(_smartAttributes);\n}\n\n\n/// Create a copy of DiskSmart\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$DiskSmartCopyWith<_DiskSmart> get copyWith => __$DiskSmartCopyWithImpl<_DiskSmart>(this, _$identity);\n\n@override\nMap<String, dynamic> toJson() {\n  return _$DiskSmartToJson(this, );\n}\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _DiskSmart&&(identical(other.device, device) || other.device == device)&&(identical(other.healthy, healthy) || other.healthy == healthy)&&(identical(other.temperature, temperature) || other.temperature == temperature)&&(identical(other.model, model) || other.model == model)&&(identical(other.serial, serial) || other.serial == serial)&&(identical(other.powerOnHours, powerOnHours) || other.powerOnHours == powerOnHours)&&(identical(other.powerCycleCount, powerCycleCount) || other.powerCycleCount == powerCycleCount)&&const DeepCollectionEquality().equals(other._rawData, _rawData)&&const DeepCollectionEquality().equals(other._smartAttributes, _smartAttributes));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,device,healthy,temperature,model,serial,powerOnHours,powerCycleCount,const DeepCollectionEquality().hash(_rawData),const DeepCollectionEquality().hash(_smartAttributes));\n\n\n\n}\n\n/// @nodoc\nabstract mixin class _$DiskSmartCopyWith<$Res> implements $DiskSmartCopyWith<$Res> {\n  factory _$DiskSmartCopyWith(_DiskSmart value, $Res Function(_DiskSmart) _then) = __$DiskSmartCopyWithImpl;\n@override @useResult\n$Res call({\n String device, bool? healthy, double? temperature, String? model, String? serial, int? powerOnHours, int? powerCycleCount, Map<String, dynamic> rawData, Map<String, SmartAttribute> smartAttributes\n});\n\n\n\n\n}\n/// @nodoc\nclass __$DiskSmartCopyWithImpl<$Res>\n    implements _$DiskSmartCopyWith<$Res> {\n  __$DiskSmartCopyWithImpl(this._self, this._then);\n\n  final _DiskSmart _self;\n  final $Res Function(_DiskSmart) _then;\n\n/// Create a copy of DiskSmart\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? device = null,Object? healthy = freezed,Object? temperature = freezed,Object? model = freezed,Object? serial = freezed,Object? powerOnHours = freezed,Object? powerCycleCount = freezed,Object? rawData = null,Object? smartAttributes = null,}) {\n  return _then(_DiskSmart(\ndevice: null == device ? _self.device : device // ignore: cast_nullable_to_non_nullable\nas String,healthy: freezed == healthy ? _self.healthy : healthy // ignore: cast_nullable_to_non_nullable\nas bool?,temperature: freezed == temperature ? _self.temperature : temperature // ignore: cast_nullable_to_non_nullable\nas double?,model: freezed == model ? _self.model : model // ignore: cast_nullable_to_non_nullable\nas String?,serial: freezed == serial ? _self.serial : serial // ignore: cast_nullable_to_non_nullable\nas String?,powerOnHours: freezed == powerOnHours ? _self.powerOnHours : powerOnHours // ignore: cast_nullable_to_non_nullable\nas int?,powerCycleCount: freezed == powerCycleCount ? _self.powerCycleCount : powerCycleCount // ignore: cast_nullable_to_non_nullable\nas int?,rawData: null == rawData ? _self._rawData : rawData // ignore: cast_nullable_to_non_nullable\nas Map<String, dynamic>,smartAttributes: null == smartAttributes ? _self._smartAttributes : smartAttributes // ignore: cast_nullable_to_non_nullable\nas Map<String, SmartAttribute>,\n  ));\n}\n\n\n}\n\n\n/// @nodoc\nmixin _$SmartAttribute {\n\n int? get id; String get name; int? get value; int? get worst; int? get thresh; String? get whenFailed; dynamic get rawValue; String? get rawString; SmartAttributeFlags get flags;\n/// Create a copy of SmartAttribute\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$SmartAttributeCopyWith<SmartAttribute> get copyWith => _$SmartAttributeCopyWithImpl<SmartAttribute>(this as SmartAttribute, _$identity);\n\n  /// Serializes this SmartAttribute to a JSON map.\n  Map<String, dynamic> toJson();\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is SmartAttribute&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.value, value) || other.value == value)&&(identical(other.worst, worst) || other.worst == worst)&&(identical(other.thresh, thresh) || other.thresh == thresh)&&(identical(other.whenFailed, whenFailed) || other.whenFailed == whenFailed)&&const DeepCollectionEquality().equals(other.rawValue, rawValue)&&(identical(other.rawString, rawString) || other.rawString == rawString)&&(identical(other.flags, flags) || other.flags == flags));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,id,name,value,worst,thresh,whenFailed,const DeepCollectionEquality().hash(rawValue),rawString,flags);\n\n\n\n}\n\n/// @nodoc\nabstract mixin class $SmartAttributeCopyWith<$Res>  {\n  factory $SmartAttributeCopyWith(SmartAttribute value, $Res Function(SmartAttribute) _then) = _$SmartAttributeCopyWithImpl;\n@useResult\n$Res call({\n int? id, String name, int? value, int? worst, int? thresh, String? whenFailed, dynamic rawValue, String? rawString, SmartAttributeFlags flags\n});\n\n\n$SmartAttributeFlagsCopyWith<$Res> get flags;\n\n}\n/// @nodoc\nclass _$SmartAttributeCopyWithImpl<$Res>\n    implements $SmartAttributeCopyWith<$Res> {\n  _$SmartAttributeCopyWithImpl(this._self, this._then);\n\n  final SmartAttribute _self;\n  final $Res Function(SmartAttribute) _then;\n\n/// Create a copy of SmartAttribute\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? name = null,Object? value = freezed,Object? worst = freezed,Object? thresh = freezed,Object? whenFailed = freezed,Object? rawValue = freezed,Object? rawString = freezed,Object? flags = null,}) {\n  return _then(_self.copyWith(\nid: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable\nas int?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable\nas String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable\nas int?,worst: freezed == worst ? _self.worst : worst // ignore: cast_nullable_to_non_nullable\nas int?,thresh: freezed == thresh ? _self.thresh : thresh // ignore: cast_nullable_to_non_nullable\nas int?,whenFailed: freezed == whenFailed ? _self.whenFailed : whenFailed // ignore: cast_nullable_to_non_nullable\nas String?,rawValue: freezed == rawValue ? _self.rawValue : rawValue // ignore: cast_nullable_to_non_nullable\nas dynamic,rawString: freezed == rawString ? _self.rawString : rawString // ignore: cast_nullable_to_non_nullable\nas String?,flags: null == flags ? _self.flags : flags // ignore: cast_nullable_to_non_nullable\nas SmartAttributeFlags,\n  ));\n}\n/// Create a copy of SmartAttribute\n/// with the given fields replaced by the non-null parameter values.\n@override\n@pragma('vm:prefer-inline')\n$SmartAttributeFlagsCopyWith<$Res> get flags {\n  \n  return $SmartAttributeFlagsCopyWith<$Res>(_self.flags, (value) {\n    return _then(_self.copyWith(flags: value));\n  });\n}\n}\n\n\n/// Adds pattern-matching-related methods to [SmartAttribute].\nextension SmartAttributePatterns on SmartAttribute {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SmartAttribute value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _SmartAttribute() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SmartAttribute value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SmartAttribute():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SmartAttribute value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SmartAttribute() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int? id,  String name,  int? value,  int? worst,  int? thresh,  String? whenFailed,  dynamic rawValue,  String? rawString,  SmartAttributeFlags flags)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _SmartAttribute() when $default != null:\nreturn $default(_that.id,_that.name,_that.value,_that.worst,_that.thresh,_that.whenFailed,_that.rawValue,_that.rawString,_that.flags);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int? id,  String name,  int? value,  int? worst,  int? thresh,  String? whenFailed,  dynamic rawValue,  String? rawString,  SmartAttributeFlags flags)  $default,) {final _that = this;\nswitch (_that) {\ncase _SmartAttribute():\nreturn $default(_that.id,_that.name,_that.value,_that.worst,_that.thresh,_that.whenFailed,_that.rawValue,_that.rawString,_that.flags);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int? id,  String name,  int? value,  int? worst,  int? thresh,  String? whenFailed,  dynamic rawValue,  String? rawString,  SmartAttributeFlags flags)?  $default,) {final _that = this;\nswitch (_that) {\ncase _SmartAttribute() when $default != null:\nreturn $default(_that.id,_that.name,_that.value,_that.worst,_that.thresh,_that.whenFailed,_that.rawValue,_that.rawString,_that.flags);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n@JsonSerializable()\n\nclass _SmartAttribute extends SmartAttribute {\n  const _SmartAttribute({this.id, required this.name, this.value, this.worst, this.thresh, this.whenFailed, this.rawValue, this.rawString, required this.flags}): super._();\n  factory _SmartAttribute.fromJson(Map<String, dynamic> json) => _$SmartAttributeFromJson(json);\n\n@override final  int? id;\n@override final  String name;\n@override final  int? value;\n@override final  int? worst;\n@override final  int? thresh;\n@override final  String? whenFailed;\n@override final  dynamic rawValue;\n@override final  String? rawString;\n@override final  SmartAttributeFlags flags;\n\n/// Create a copy of SmartAttribute\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$SmartAttributeCopyWith<_SmartAttribute> get copyWith => __$SmartAttributeCopyWithImpl<_SmartAttribute>(this, _$identity);\n\n@override\nMap<String, dynamic> toJson() {\n  return _$SmartAttributeToJson(this, );\n}\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SmartAttribute&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.value, value) || other.value == value)&&(identical(other.worst, worst) || other.worst == worst)&&(identical(other.thresh, thresh) || other.thresh == thresh)&&(identical(other.whenFailed, whenFailed) || other.whenFailed == whenFailed)&&const DeepCollectionEquality().equals(other.rawValue, rawValue)&&(identical(other.rawString, rawString) || other.rawString == rawString)&&(identical(other.flags, flags) || other.flags == flags));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,id,name,value,worst,thresh,whenFailed,const DeepCollectionEquality().hash(rawValue),rawString,flags);\n\n\n\n}\n\n/// @nodoc\nabstract mixin class _$SmartAttributeCopyWith<$Res> implements $SmartAttributeCopyWith<$Res> {\n  factory _$SmartAttributeCopyWith(_SmartAttribute value, $Res Function(_SmartAttribute) _then) = __$SmartAttributeCopyWithImpl;\n@override @useResult\n$Res call({\n int? id, String name, int? value, int? worst, int? thresh, String? whenFailed, dynamic rawValue, String? rawString, SmartAttributeFlags flags\n});\n\n\n@override $SmartAttributeFlagsCopyWith<$Res> get flags;\n\n}\n/// @nodoc\nclass __$SmartAttributeCopyWithImpl<$Res>\n    implements _$SmartAttributeCopyWith<$Res> {\n  __$SmartAttributeCopyWithImpl(this._self, this._then);\n\n  final _SmartAttribute _self;\n  final $Res Function(_SmartAttribute) _then;\n\n/// Create a copy of SmartAttribute\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? name = null,Object? value = freezed,Object? worst = freezed,Object? thresh = freezed,Object? whenFailed = freezed,Object? rawValue = freezed,Object? rawString = freezed,Object? flags = null,}) {\n  return _then(_SmartAttribute(\nid: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable\nas int?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable\nas String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable\nas int?,worst: freezed == worst ? _self.worst : worst // ignore: cast_nullable_to_non_nullable\nas int?,thresh: freezed == thresh ? _self.thresh : thresh // ignore: cast_nullable_to_non_nullable\nas int?,whenFailed: freezed == whenFailed ? _self.whenFailed : whenFailed // ignore: cast_nullable_to_non_nullable\nas String?,rawValue: freezed == rawValue ? _self.rawValue : rawValue // ignore: cast_nullable_to_non_nullable\nas dynamic,rawString: freezed == rawString ? _self.rawString : rawString // ignore: cast_nullable_to_non_nullable\nas String?,flags: null == flags ? _self.flags : flags // ignore: cast_nullable_to_non_nullable\nas SmartAttributeFlags,\n  ));\n}\n\n/// Create a copy of SmartAttribute\n/// with the given fields replaced by the non-null parameter values.\n@override\n@pragma('vm:prefer-inline')\n$SmartAttributeFlagsCopyWith<$Res> get flags {\n  \n  return $SmartAttributeFlagsCopyWith<$Res>(_self.flags, (value) {\n    return _then(_self.copyWith(flags: value));\n  });\n}\n}\n\n\n/// @nodoc\nmixin _$SmartAttributeFlags {\n\n int? get value; String? get string; bool get prefailure; bool get updatedOnline; bool get performance; bool get errorRate; bool get eventCount; bool get autoKeep;\n/// Create a copy of SmartAttributeFlags\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$SmartAttributeFlagsCopyWith<SmartAttributeFlags> get copyWith => _$SmartAttributeFlagsCopyWithImpl<SmartAttributeFlags>(this as SmartAttributeFlags, _$identity);\n\n  /// Serializes this SmartAttributeFlags to a JSON map.\n  Map<String, dynamic> toJson();\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is SmartAttributeFlags&&(identical(other.value, value) || other.value == value)&&(identical(other.string, string) || other.string == string)&&(identical(other.prefailure, prefailure) || other.prefailure == prefailure)&&(identical(other.updatedOnline, updatedOnline) || other.updatedOnline == updatedOnline)&&(identical(other.performance, performance) || other.performance == performance)&&(identical(other.errorRate, errorRate) || other.errorRate == errorRate)&&(identical(other.eventCount, eventCount) || other.eventCount == eventCount)&&(identical(other.autoKeep, autoKeep) || other.autoKeep == autoKeep));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,value,string,prefailure,updatedOnline,performance,errorRate,eventCount,autoKeep);\n\n\n\n}\n\n/// @nodoc\nabstract mixin class $SmartAttributeFlagsCopyWith<$Res>  {\n  factory $SmartAttributeFlagsCopyWith(SmartAttributeFlags value, $Res Function(SmartAttributeFlags) _then) = _$SmartAttributeFlagsCopyWithImpl;\n@useResult\n$Res call({\n int? value, String? string, bool prefailure, bool updatedOnline, bool performance, bool errorRate, bool eventCount, bool autoKeep\n});\n\n\n\n\n}\n/// @nodoc\nclass _$SmartAttributeFlagsCopyWithImpl<$Res>\n    implements $SmartAttributeFlagsCopyWith<$Res> {\n  _$SmartAttributeFlagsCopyWithImpl(this._self, this._then);\n\n  final SmartAttributeFlags _self;\n  final $Res Function(SmartAttributeFlags) _then;\n\n/// Create a copy of SmartAttributeFlags\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? value = freezed,Object? string = freezed,Object? prefailure = null,Object? updatedOnline = null,Object? performance = null,Object? errorRate = null,Object? eventCount = null,Object? autoKeep = null,}) {\n  return _then(_self.copyWith(\nvalue: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable\nas int?,string: freezed == string ? _self.string : string // ignore: cast_nullable_to_non_nullable\nas String?,prefailure: null == prefailure ? _self.prefailure : prefailure // ignore: cast_nullable_to_non_nullable\nas bool,updatedOnline: null == updatedOnline ? _self.updatedOnline : updatedOnline // ignore: cast_nullable_to_non_nullable\nas bool,performance: null == performance ? _self.performance : performance // ignore: cast_nullable_to_non_nullable\nas bool,errorRate: null == errorRate ? _self.errorRate : errorRate // ignore: cast_nullable_to_non_nullable\nas bool,eventCount: null == eventCount ? _self.eventCount : eventCount // ignore: cast_nullable_to_non_nullable\nas bool,autoKeep: null == autoKeep ? _self.autoKeep : autoKeep // ignore: cast_nullable_to_non_nullable\nas bool,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [SmartAttributeFlags].\nextension SmartAttributeFlagsPatterns on SmartAttributeFlags {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SmartAttributeFlags value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _SmartAttributeFlags() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SmartAttributeFlags value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SmartAttributeFlags():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SmartAttributeFlags value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SmartAttributeFlags() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int? value,  String? string,  bool prefailure,  bool updatedOnline,  bool performance,  bool errorRate,  bool eventCount,  bool autoKeep)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _SmartAttributeFlags() when $default != null:\nreturn $default(_that.value,_that.string,_that.prefailure,_that.updatedOnline,_that.performance,_that.errorRate,_that.eventCount,_that.autoKeep);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int? value,  String? string,  bool prefailure,  bool updatedOnline,  bool performance,  bool errorRate,  bool eventCount,  bool autoKeep)  $default,) {final _that = this;\nswitch (_that) {\ncase _SmartAttributeFlags():\nreturn $default(_that.value,_that.string,_that.prefailure,_that.updatedOnline,_that.performance,_that.errorRate,_that.eventCount,_that.autoKeep);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int? value,  String? string,  bool prefailure,  bool updatedOnline,  bool performance,  bool errorRate,  bool eventCount,  bool autoKeep)?  $default,) {final _that = this;\nswitch (_that) {\ncase _SmartAttributeFlags() when $default != null:\nreturn $default(_that.value,_that.string,_that.prefailure,_that.updatedOnline,_that.performance,_that.errorRate,_that.eventCount,_that.autoKeep);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n@JsonSerializable()\n\nclass _SmartAttributeFlags extends SmartAttributeFlags {\n  const _SmartAttributeFlags({this.value, this.string, this.prefailure = false, this.updatedOnline = false, this.performance = false, this.errorRate = false, this.eventCount = false, this.autoKeep = false}): super._();\n  factory _SmartAttributeFlags.fromJson(Map<String, dynamic> json) => _$SmartAttributeFlagsFromJson(json);\n\n@override final  int? value;\n@override final  String? string;\n@override@JsonKey() final  bool prefailure;\n@override@JsonKey() final  bool updatedOnline;\n@override@JsonKey() final  bool performance;\n@override@JsonKey() final  bool errorRate;\n@override@JsonKey() final  bool eventCount;\n@override@JsonKey() final  bool autoKeep;\n\n/// Create a copy of SmartAttributeFlags\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$SmartAttributeFlagsCopyWith<_SmartAttributeFlags> get copyWith => __$SmartAttributeFlagsCopyWithImpl<_SmartAttributeFlags>(this, _$identity);\n\n@override\nMap<String, dynamic> toJson() {\n  return _$SmartAttributeFlagsToJson(this, );\n}\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SmartAttributeFlags&&(identical(other.value, value) || other.value == value)&&(identical(other.string, string) || other.string == string)&&(identical(other.prefailure, prefailure) || other.prefailure == prefailure)&&(identical(other.updatedOnline, updatedOnline) || other.updatedOnline == updatedOnline)&&(identical(other.performance, performance) || other.performance == performance)&&(identical(other.errorRate, errorRate) || other.errorRate == errorRate)&&(identical(other.eventCount, eventCount) || other.eventCount == eventCount)&&(identical(other.autoKeep, autoKeep) || other.autoKeep == autoKeep));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,value,string,prefailure,updatedOnline,performance,errorRate,eventCount,autoKeep);\n\n\n\n}\n\n/// @nodoc\nabstract mixin class _$SmartAttributeFlagsCopyWith<$Res> implements $SmartAttributeFlagsCopyWith<$Res> {\n  factory _$SmartAttributeFlagsCopyWith(_SmartAttributeFlags value, $Res Function(_SmartAttributeFlags) _then) = __$SmartAttributeFlagsCopyWithImpl;\n@override @useResult\n$Res call({\n int? value, String? string, bool prefailure, bool updatedOnline, bool performance, bool errorRate, bool eventCount, bool autoKeep\n});\n\n\n\n\n}\n/// @nodoc\nclass __$SmartAttributeFlagsCopyWithImpl<$Res>\n    implements _$SmartAttributeFlagsCopyWith<$Res> {\n  __$SmartAttributeFlagsCopyWithImpl(this._self, this._then);\n\n  final _SmartAttributeFlags _self;\n  final $Res Function(_SmartAttributeFlags) _then;\n\n/// Create a copy of SmartAttributeFlags\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? value = freezed,Object? string = freezed,Object? prefailure = null,Object? updatedOnline = null,Object? performance = null,Object? errorRate = null,Object? eventCount = null,Object? autoKeep = null,}) {\n  return _then(_SmartAttributeFlags(\nvalue: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable\nas int?,string: freezed == string ? _self.string : string // ignore: cast_nullable_to_non_nullable\nas String?,prefailure: null == prefailure ? _self.prefailure : prefailure // ignore: cast_nullable_to_non_nullable\nas bool,updatedOnline: null == updatedOnline ? _self.updatedOnline : updatedOnline // ignore: cast_nullable_to_non_nullable\nas bool,performance: null == performance ? _self.performance : performance // ignore: cast_nullable_to_non_nullable\nas bool,errorRate: null == errorRate ? _self.errorRate : errorRate // ignore: cast_nullable_to_non_nullable\nas bool,eventCount: null == eventCount ? _self.eventCount : eventCount // ignore: cast_nullable_to_non_nullable\nas bool,autoKeep: null == autoKeep ? _self.autoKeep : autoKeep // ignore: cast_nullable_to_non_nullable\nas bool,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/model/server/disk_smart.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'disk_smart.dart';\n\n// **************************************************************************\n// JsonSerializableGenerator\n// **************************************************************************\n\n_DiskSmart _$DiskSmartFromJson(Map<String, dynamic> json) => _DiskSmart(\n  device: json['device'] as String,\n  healthy: json['healthy'] as bool?,\n  temperature: (json['temperature'] as num?)?.toDouble(),\n  model: json['model'] as String?,\n  serial: json['serial'] as String?,\n  powerOnHours: (json['powerOnHours'] as num?)?.toInt(),\n  powerCycleCount: (json['powerCycleCount'] as num?)?.toInt(),\n  rawData: json['rawData'] as Map<String, dynamic>,\n  smartAttributes: (json['smartAttributes'] as Map<String, dynamic>).map(\n    (k, e) => MapEntry(k, SmartAttribute.fromJson(e as Map<String, dynamic>)),\n  ),\n);\n\nMap<String, dynamic> _$DiskSmartToJson(_DiskSmart instance) =>\n    <String, dynamic>{\n      'device': instance.device,\n      'healthy': instance.healthy,\n      'temperature': instance.temperature,\n      'model': instance.model,\n      'serial': instance.serial,\n      'powerOnHours': instance.powerOnHours,\n      'powerCycleCount': instance.powerCycleCount,\n      'rawData': instance.rawData,\n      'smartAttributes': instance.smartAttributes,\n    };\n\n_SmartAttribute _$SmartAttributeFromJson(Map<String, dynamic> json) =>\n    _SmartAttribute(\n      id: (json['id'] as num?)?.toInt(),\n      name: json['name'] as String,\n      value: (json['value'] as num?)?.toInt(),\n      worst: (json['worst'] as num?)?.toInt(),\n      thresh: (json['thresh'] as num?)?.toInt(),\n      whenFailed: json['whenFailed'] as String?,\n      rawValue: json['rawValue'],\n      rawString: json['rawString'] as String?,\n      flags: SmartAttributeFlags.fromJson(\n        json['flags'] as Map<String, dynamic>,\n      ),\n    );\n\nMap<String, dynamic> _$SmartAttributeToJson(_SmartAttribute instance) =>\n    <String, dynamic>{\n      'id': instance.id,\n      'name': instance.name,\n      'value': instance.value,\n      'worst': instance.worst,\n      'thresh': instance.thresh,\n      'whenFailed': instance.whenFailed,\n      'rawValue': instance.rawValue,\n      'rawString': instance.rawString,\n      'flags': instance.flags,\n    };\n\n_SmartAttributeFlags _$SmartAttributeFlagsFromJson(Map<String, dynamic> json) =>\n    _SmartAttributeFlags(\n      value: (json['value'] as num?)?.toInt(),\n      string: json['string'] as String?,\n      prefailure: json['prefailure'] as bool? ?? false,\n      updatedOnline: json['updatedOnline'] as bool? ?? false,\n      performance: json['performance'] as bool? ?? false,\n      errorRate: json['errorRate'] as bool? ?? false,\n      eventCount: json['eventCount'] as bool? ?? false,\n      autoKeep: json['autoKeep'] as bool? ?? false,\n    );\n\nMap<String, dynamic> _$SmartAttributeFlagsToJson(\n  _SmartAttributeFlags instance,\n) => <String, dynamic>{\n  'value': instance.value,\n  'string': instance.string,\n  'prefailure': instance.prefailure,\n  'updatedOnline': instance.updatedOnline,\n  'performance': instance.performance,\n  'errorRate': instance.errorRate,\n  'eventCount': instance.eventCount,\n  'autoKeep': instance.autoKeep,\n};\n"
  },
  {
    "path": "lib/data/model/server/dist.dart",
    "content": "enum Dist {\n  debian,\n  ubuntu,\n  centos,\n  fedora,\n  opensuse,\n  kali,\n  wrt,\n  armbian,\n  arch,\n  alpine,\n  rocky,\n  deepin,\n  coreelec,\n}\n\nextension StringX on String {\n  Dist? get dist {\n    final lower = toLowerCase();\n    for (final dist in Dist.values) {\n      if (lower.contains(dist.name)) {\n        return dist;\n      }\n    }\n    for (final wrt in _wrts) {\n      if (lower.contains(wrt)) {\n        return Dist.wrt;\n      }\n    }\n    return null;\n  }\n}\n\n// Special rules\n\nconst _wrts = ['istoreos'];\n"
  },
  {
    "path": "lib/data/model/server/memory.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\n\nclass Memory {\n  final int total;\n  final int free;\n  final int avail;\n\n  const Memory({required this.total, required this.free, required this.avail});\n\n  double get availPercent {\n    if (avail == 0) {\n      return free / total;\n    }\n    return avail / total;\n  }\n\n  double get usedPercent => 1 - availPercent;\n\n  static Memory parse(String raw) {\n    final items = raw.split('\\n').map((e) => memItemReg.firstMatch(e)).toList();\n\n    final total =\n        int.tryParse(items.firstWhereOrNull((e) => e?.group(1) == 'MemTotal:')?.group(2) ?? '1') ?? 1;\n    final free = int.tryParse(items.firstWhereOrNull((e) => e?.group(1) == 'MemFree:')?.group(2) ?? '0') ?? 0;\n    final available =\n        int.tryParse(items.firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:')?.group(2) ?? '0') ?? 0;\n\n    return Memory(total: total, free: free, avail: available);\n  }\n}\n\nfinal memItemReg = RegExp(r'([A-Z].+:)\\s+([0-9]+) kB');\n\n/// Parse BSD/macOS memory from top output\n///\n/// Supports formats like:\n/// - macOS: \"PhysMem: 32G used (1536M wired), 64G unused.\"\n/// - FreeBSD: \"Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free\"\nMemory parseBsdMemory(String raw) {\n  // Try macOS format first: \"PhysMem: 32G used (1536M wired), 64G unused.\"\n  final macMemReg = RegExp(r'PhysMem:\\s*([\\d.]+)([KMGT])\\s*used.*?,\\s*([\\d.]+)([KMGT])\\s*unused');\n  final macMatch = macMemReg.firstMatch(raw);\n\n  if (macMatch != null) {\n    final usedAmount = double.parse(macMatch.group(1)!);\n    final usedUnit = macMatch.group(2)!;\n    final freeAmount = double.parse(macMatch.group(3)!);\n    final freeUnit = macMatch.group(4)!;\n\n    final usedKB = _convertToKB(usedAmount, usedUnit);\n    final freeKB = _convertToKB(freeAmount, freeUnit);\n    return Memory(total: usedKB + freeKB, free: freeKB, avail: freeKB);\n  }\n\n  // Try FreeBSD format: \"Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free\"\n  final freeBsdReg = RegExp(r'(\\d+)([KMGT])\\s+(Active|Inact|Wired|Cache|Buf|Free)', caseSensitive: false);\n  final matches = freeBsdReg.allMatches(raw);\n\n  if (matches.isNotEmpty) {\n    double usedKB = 0;\n    double freeKB = 0;\n    for (final match in matches) {\n      final amount = double.parse(match.group(1)!);\n      final unit = match.group(2)!;\n      final keyword = match.group(3)!.toLowerCase();\n      final kb = _convertToKB(amount, unit);\n\n      // Only sum known keywords\n      if (keyword == 'active' ||\n          keyword == 'inact' ||\n          keyword == 'wired' ||\n          keyword == 'cache' ||\n          keyword == 'buf') {\n        usedKB += kb;\n      } else if (keyword == 'free') {\n        freeKB += kb;\n      }\n    }\n    return Memory(total: (usedKB + freeKB).round(), free: freeKB.round(), avail: freeKB.round());\n  }\n\n  // If neither format matches, throw an error to avoid misinterpretation\n  throw FormatException('Unrecognized BSD/macOS memory format: $raw');\n}\n\n/// Convert memory size to KB based on unit\nint _convertToKB(double amount, String unit) {\n  switch (unit.toUpperCase()) {\n    case 'T':\n      return (amount * 1024 * 1024 * 1024).round();\n    case 'G':\n      return (amount * 1024 * 1024).round();\n    case 'M':\n      return (amount * 1024).round();\n    case 'K':\n    case '':\n      return amount.round();\n    default:\n      return amount.round();\n  }\n}\n\nclass Swap {\n  final int total;\n  final int free;\n  final int cached;\n\n  const Swap({required this.total, required this.free, required this.cached});\n\n  double get usedPercent => total == 0 ? 0.0 : 1 - free / total;\n\n  double get freePercent => total == 0 ? 0.0 : free / total;\n\n  @override\n  String toString() {\n    return 'Swap{total: $total, free: $free, cached: $cached}';\n  }\n\n  static Swap parse(String raw) {\n    final items = raw.split('\\n').map((e) => memItemReg.firstMatch(e)).toList();\n\n    final total =\n        int.tryParse(items.firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:')?.group(2) ?? '1') ?? 0;\n    final free =\n        int.tryParse(items.firstWhereOrNull((e) => e?.group(1) == 'SwapFree:')?.group(2) ?? '1') ?? 0;\n    final cached =\n        int.tryParse(items.firstWhereOrNull((e) => e?.group(1) == 'SwapCached:')?.group(2) ?? '0') ?? 0;\n\n    return Swap(total: total, free: free, cached: cached);\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/net_speed.dart",
    "content": "// ignore_for_file: unintended_html_in_doc_comment\n\nimport 'package:fl_lib/fl_lib.dart';\n\nimport 'package:server_box/data/model/server/time_seq.dart';\n\nclass NetSpeedPart extends TimeSeqIface<NetSpeedPart> {\n  final String device;\n  final BigInt bytesIn;\n  final BigInt bytesOut;\n  final int time;\n\n  NetSpeedPart(this.device, this.bytesIn, this.bytesOut, this.time);\n\n  @override\n  bool same(NetSpeedPart other) => device == other.device;\n}\n\ntypedef CachedNetVals = ({String sizeIn, String sizeOut, String speedIn, String speedOut});\n\nclass NetSpeed extends TimeSeq<NetSpeedPart> {\n  NetSpeed(super.init1, super.init2);\n\n  @override\n  void onUpdate() {\n    devices.clear();\n    devices.addAll(now.map((e) => e.device).toList());\n\n    realIfaces.clear();\n    realIfaces.addAll(devices.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));\n\n    final sizeIn = this.sizeIn();\n    final sizeOut = this.sizeOut();\n    final speedIn = this.speedIn();\n    final speedOut = this.speedOut();\n\n    cachedVals = (sizeIn: sizeIn, sizeOut: sizeOut, speedIn: speedIn, speedOut: speedOut);\n  }\n\n  /// Cached network device list\n  final devices = <String>[];\n\n  /// Issue #295\n  /// Non-virtual network device prefix\n  static const realIfacePrefixs = ['eth', 'wlan', 'en', 'ww', 'wl'];\n\n  /// Cached non-virtual network device prefix\n  final realIfaces = <String>[];\n\n  CachedNetVals cachedVals = (sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');\n\n  /// Time diff between [pre] and [now]\n  BigInt get _timeDiff => BigInt.from(now[0].time - pre[0].time);\n\n  double speedInBytes(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff;\n  double speedOutBytes(int i) => (now[i].bytesOut - pre[i].bytesOut) / _timeDiff;\n  BigInt sizeInBytes(int i) => now[i].bytesIn;\n  BigInt sizeOutBytes(int i) => now[i].bytesOut;\n\n  String speedIn({String? device}) {\n    if (pre.isEmpty || now.isEmpty) return 'N/A';\n    if (pre.length != now.length) return 'N/A';\n    if (device == null) {\n      var speed = 0.0;\n      for (final device in devices) {\n        for (final prefix in realIfacePrefixs) {\n          if (device.startsWith(prefix)) {\n            speed += speedInBytes(devices.indexOf(device));\n          }\n        }\n      }\n      return buildStandardOutput(speed);\n    }\n    final idx = deviceIdx(device);\n    return buildStandardOutput(speedInBytes(idx));\n  }\n\n  String sizeIn({String? device}) {\n    if (pre.isEmpty || now.isEmpty) return 'N/A';\n    if (pre.length != now.length) return 'N/A';\n    if (device == null) {\n      var size = BigInt.from(0);\n      for (final device in devices) {\n        for (final prefix in realIfacePrefixs) {\n          if (device.startsWith(prefix)) {\n            size += sizeInBytes(devices.indexOf(device));\n          }\n        }\n      }\n      return size.bytes2Str;\n    }\n    final idx = deviceIdx(device);\n    return sizeInBytes(idx).bytes2Str;\n  }\n\n  String speedOut({String? device}) {\n    if (pre.isEmpty || now.isEmpty) return 'N/A';\n    if (pre.length != now.length) return 'N/A';\n    if (device == null) {\n      var speed = 0.0;\n      for (final device in devices) {\n        for (final prefix in realIfacePrefixs) {\n          if (device.startsWith(prefix)) {\n            speed += speedOutBytes(devices.indexOf(device));\n          }\n        }\n      }\n      return buildStandardOutput(speed);\n    }\n    final idx = deviceIdx(device);\n    return buildStandardOutput(speedOutBytes(idx));\n  }\n\n  String sizeOut({String? device}) {\n    if (pre.isEmpty || now.isEmpty) return 'N/A';\n    if (pre.length != now.length) return 'N/A';\n    if (device == null) {\n      var size = BigInt.from(0);\n      for (final device in devices) {\n        for (final prefix in realIfacePrefixs) {\n          if (device.startsWith(prefix)) {\n            size += sizeOutBytes(devices.indexOf(device));\n          }\n        }\n      }\n      return size.bytes2Str;\n    }\n    final idx = deviceIdx(device);\n    return sizeOutBytes(idx).bytes2Str;\n  }\n\n  int deviceIdx(String? device) {\n    if (device != null) {\n      for (var item in now) {\n        if (item.device == device) {\n          return now.indexOf(item);\n        }\n      }\n    }\n    return 0;\n  }\n\n  String buildStandardOutput(double speed) => '${speed.bytes2Str}/s';\n\n  /// [raw] example:\n  /// Inter-|   Receive                                                |  Transmit\n  ///   face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed\n  ///   lo: 45929941  269112    0    0    0     0          0         0 45929941  269112    0    0    0     0       0          0\n  ///   eth0: 48481023  505772    0    0    0     0          0         0 36002262  202307    0    0    0     0       0          0\n  static List<NetSpeedPart> parse(String raw, int time) {\n    final split = raw.split('\\n');\n    if (split.length < 4) {\n      return [];\n    }\n\n    final results = <NetSpeedPart>[];\n    for (final item in split.sublist(2)) {\n      try {\n        final data = item.trim().split(':');\n        final device = data.firstOrNull;\n        if (device == null) continue;\n        final bytes = data.last.trim().split(' ');\n        bytes.removeWhere((element) => element == '');\n        final bytesIn = BigInt.parse(bytes.first);\n        final bytesOut = BigInt.parse(bytes[8]);\n        results.add(NetSpeedPart(device, bytesIn, bytesOut, time));\n      } catch (e, s) {\n        Loggers.app.warning('Failed to parse net speed data: $item', e, s);\n        continue;\n      }\n    }\n    return results;\n  }\n\n  /// [raw] example:\n  /// Name       Mtu   Network       Address            Ipkts Ierrs     Ibytes    Opkts Oerrs     Obytes  Coll\n  /// lo0        16384 <Link#1>                      17296531     0 2524959720 17296531     0 2524959720     0\n  /// lo0        16384 127           127.0.0.1       17296531     - 2524959720 17296531     - 2524959720     -\n  /// lo0        16384 ::1/128     ::1               17296531     - 2524959720 17296531     - 2524959720     -\n  /// lo0        16384 fe80::1%lo0 fe80:1::1         17296531     - 2524959720 17296531     - 2524959720     -\n  /// gif0*      1280  <Link#2>                             0     0          0        0     0          0     0\n  /// stf0*      1280  <Link#3>                             0     0          0        0     0          0     0\n  /// en0        1500  <Link#4>    22:20:xx:xx:xx:e6   739447     0  693997876   535600     0   79008877     0\n  /// en0        1500  fe80::f1:xx fe80:4::f1:xxxx:9   739447     -  693997876   535600     -   79008877     -\n  /// en0        1500  192.168.2     192.168.2.111     739447     -  693997876   535600     -   79008877     -\n  /// en0        1500  fd6b:xxxx:3 fd6b:xxxx:xxxx:0:   739447     -  693997876   535600     -   79008877     -\n  /// en1        1500  <Link#5>    88:d8:xx:xx:xx:1d        0     0          0        0     0          0     0\n  /// utun0      1380  <Link#6>                             0     0          0        3     0        280     0\n  /// utun0      1380  fe80::xxxx: fe80:6::xxxx:xxxx        0     -          0        3     -        280     -\n  /// utun1      2000  <Link#7>                             0     0          0        3     0        280     0\n  /// utun1      2000  fe80::xxxx: fe80:7::xxxx:xxxx        0     -          0        3     -        280     -\n  /// utun2      1000  <Link#8>                             0     0          0        3     0        280     0\n  /// utun2      1000  fe80::xxxx: fe80:8::xxxx:xxx:        0     -          0        3     -        280     -\n  /// utun4      9000  <Link#10>                       746744     0  845373390   386111     0  424400998     0\n  /// utun4      9000  198.18.0/16   198.18.0.1        746744     -  845373390   386111     -  424400998     -\n  /// en2*       1500  <Link#11>   36:7c:xx:xx:xx:xx        0     0          0        0     0          0     0\n  static List<NetSpeedPart> parseBsd(String raw, int time) {\n    final split = raw.split('\\n');\n    if (split.length < 2) {\n      return [];\n    }\n\n    final results = <NetSpeedPart>[];\n    for (final item in split.sublist(1)) {\n      final data = item.trim().split(RegExp(r'\\s+'));\n      final device = data[0];\n      if (device.endsWith('*')) {\n        continue;\n      }\n      if (results.any((element) => element.device == device)) {\n        continue;\n      }\n      if (data.length != 11) {\n        continue;\n      }\n      final bytesIn = BigInt.parse(data[6]);\n      final bytesOut = BigInt.parse(data[9]);\n      results.add(NetSpeedPart(device, bytesIn, bytesOut, time));\n    }\n    return results;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/nvdia.dart",
    "content": "import 'package:xml/xml.dart';\n\n/// [\n///   {\n///     \"name\": \"GeForce RTX 3090\",\n///     \"temp\": 40,\n///     \"power\": \"30W / 350W\",\n///     \"memory\": {\n///       \"total\": 24268,\n///       \"used\": 240,\n///       \"unit\": \"MiB\",\n///       \"processes\": [\n///         {\n///           \"pid\": 1456,\n///           \"name\": \"/usr/lib/xorg/Xorg\",\n///           \"memory\": 40\n///         },\n///       ]\n///     },\n///   }\n/// ]\n///\n\nclass NvidiaSmi {\n  static List<NvidiaSmiItem> fromXml(String raw) {\n    final xmlData = XmlDocument.parse(raw);\n    final gpus = xmlData.findAllElements('gpu');\n    final result = List<NvidiaSmiItem?>.generate(gpus.length, (index) {\n      final gpu = gpus.elementAt(index);\n      final name = gpu.findElements('product_name').firstOrNull?.innerText;\n      final temp = gpu\n          .findElements('temperature')\n          .firstOrNull\n          ?.findElements('gpu_temp')\n          .firstOrNull\n          ?.innerText;\n      final power = gpu.findElements('gpu_power_readings').firstOrNull;\n      final powerDraw = power?.findElements('power_draw').firstOrNull?.innerText;\n      final powerLimit = power?.findElements('current_power_limit').firstOrNull?.innerText;\n      final memory = gpu.findElements('fb_memory_usage').firstOrNull;\n      final memoryUsed = memory?.findElements('used').firstOrNull?.innerText;\n      final memoryTotal = memory?.findElements('total').firstOrNull?.innerText;\n      final processes = gpu.findElements('processes').firstOrNull?.findElements('process_info');\n      final memoryProcesses = List<NvidiaSmiMemProcess?>.generate(processes?.length ?? 0, (index) {\n        final process = processes?.elementAt(index);\n        final pid = process?.findElements('pid').firstOrNull?.innerText;\n        final name = process?.findElements('process_name').firstOrNull?.innerText;\n        final memory = process?.findElements('used_memory').firstOrNull?.innerText;\n        if (pid != null && name != null && memory != null) {\n          return NvidiaSmiMemProcess(\n            int.tryParse(pid) ?? 0,\n            name,\n            int.tryParse(memory.split(' ').firstOrNull ?? '0') ?? 0,\n          );\n        }\n        return null;\n      });\n      memoryProcesses.removeWhere((element) => element == null);\n      final percent = gpu\n          .findElements('utilization')\n          .firstOrNull\n          ?.findElements('gpu_util')\n          .firstOrNull\n          ?.innerText;\n      final fanSpeed = gpu.findElements('fan_speed').firstOrNull?.innerText;\n      if (name != null && temp != null) {\n        return NvidiaSmiItem(\n          name: name,\n          uuid: gpu.findElements('uuid').firstOrNull?.innerText ?? '',\n          temp: int.tryParse(temp.split(' ').firstOrNull ?? '0') ?? 0,\n          percent: int.tryParse(percent?.split(' ').firstOrNull ?? '0') ?? 0,\n          power: '$powerDraw / $powerLimit',\n          memory: NvidiaSmiMem(\n            int.tryParse(memoryTotal?.split(' ').firstOrNull ?? '0') ?? 0,\n            int.tryParse(memoryUsed?.split(' ').firstOrNull ?? '0') ?? 0,\n            'MiB',\n            List.from(memoryProcesses),\n          ),\n          fanSpeed: int.tryParse(fanSpeed?.split(' ').firstOrNull ?? '0') ?? 0,\n        );\n      }\n      return null;\n    });\n    result.removeWhere((element) => element == null);\n    return List.from(result);\n  }\n}\n\nclass NvidiaSmiItem {\n  final String uuid;\n  final String name;\n  final int temp;\n  final String power;\n  final NvidiaSmiMem memory;\n  final int percent;\n  final int fanSpeed;\n\n  const NvidiaSmiItem({\n    required this.uuid,\n    required this.name,\n    required this.temp,\n    required this.power,\n    required this.memory,\n    required this.percent,\n    required this.fanSpeed,\n  });\n\n  @override\n  String toString() {\n    return 'NvdiaSmiItem{name: $name, temp: $temp, power: $power, memory: $memory}';\n  }\n}\n\nclass NvidiaSmiMem {\n  final int total;\n  final int used;\n  final String unit;\n  final List<NvidiaSmiMemProcess> processes;\n\n  const NvidiaSmiMem(this.total, this.used, this.unit, this.processes);\n\n  @override\n  String toString() {\n    return 'NvdiaSmiMem{total: $total, used: $used, unit: $unit, processes: $processes}';\n  }\n}\n\nclass NvidiaSmiMemProcess {\n  final int pid;\n  final String name;\n  final int memory;\n\n  const NvidiaSmiMemProcess(this.pid, this.name, this.memory);\n\n  @override\n  String toString() {\n    return 'NvdiaSmiMemProcess{pid: $pid, name: $name, memory: $memory}';\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/ping_result.dart",
    "content": "final parseFailed = Exception('Parse failed');\nfinal seqReg = RegExp(r'seq=(.+) ttl=(.+) time=(.+) ms');\nfinal packetReg = RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');\nfinal timeReg = RegExp(r'min/avg/max/mdev = (.+)/(.+)/(.+)/(.+) ms');\nfinal timeAlpineReg = RegExp(r'round-trip min/avg/max = (.+)/(.+)/(.+) ms');\nfinal ipReg = RegExp(r' \\((\\S+)\\)');\n\nclass PingResult {\n  String serverName;\n  String? ip;\n  List<PingSeqResult>? results;\n  PingStatistics? statistic;\n\n  PingResult.parse(this.serverName, String raw) {\n    final lines = raw.split('\\n');\n    lines.removeWhere((element) => element.isEmpty);\n    final statisticIndex = lines.indexWhere((element) => element.startsWith('---'));\n    if (statisticIndex == -1) {\n      throw parseFailed;\n    }\n    final statisticRaw = lines.sublist(statisticIndex + 1);\n    statistic = PingStatistics.parse(statisticRaw);\n    results = lines.sublist(1, statisticIndex).map((e) => PingSeqResult.parse(e)).toList();\n    ip = ipReg.firstMatch(lines[0])?.group(1);\n  }\n}\n\nclass PingSeqResult {\n  int? seq;\n  int? ttl;\n  double? time;\n\n  PingSeqResult.parse(String raw) {\n    final seqMatched = seqReg.firstMatch(raw);\n    if (seqMatched == null) {\n      throw parseFailed;\n    }\n    seq = int.tryParse(seqMatched.group(1)!);\n    ttl = int.tryParse(seqMatched.group(2)!);\n    time = double.tryParse(seqMatched.group(3)!);\n  }\n\n  @override\n  String toString() {\n    return 'seq: $seq, ttl: $ttl, time: $time';\n  }\n}\n\nclass PingStatistics {\n  int? total;\n  int? received;\n  double? loss;\n  double? min;\n  double? max;\n  double? avg;\n  double? stddev;\n\n  PingStatistics.parse(List<String> lines) {\n    if (lines.isEmpty || lines.length != 2) {\n      return;\n    }\n    final packetMatched = packetReg.firstMatch(lines[0]);\n    final timeMatched = timeReg.firstMatch(lines[1]);\n    final timeAlpineMatched = timeAlpineReg.firstMatch(lines[1]);\n    if (packetMatched == null) {\n      return;\n    }\n    total = int.tryParse(packetMatched.group(1)!);\n    received = int.tryParse(packetMatched.group(2)!);\n    loss = double.tryParse(packetMatched.group(3)!);\n    if (timeMatched != null) {\n      min = double.tryParse(timeMatched.group(1)!);\n      avg = double.tryParse(timeMatched.group(2)!);\n      max = double.tryParse(timeMatched.group(3)!);\n      stddev = double.tryParse(timeMatched.group(4)!);\n    } else if (timeAlpineMatched != null) {\n      min = double.tryParse(timeAlpineMatched.group(1)!);\n      avg = double.tryParse(timeAlpineMatched.group(2)!);\n      max = double.tryParse(timeAlpineMatched.group(3)!);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/private_key_info.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'private_key_info.g.dart';\n\n@JsonSerializable()\nclass PrivateKeyInfo {\n  final String id;\n  @JsonKey(name: 'private_key')\n  final String key;\n\n  const PrivateKeyInfo({required this.id, required this.key});\n\n  factory PrivateKeyInfo.fromJson(Map<String, dynamic> json) => _$PrivateKeyInfoFromJson(json);\n\n  Map<String, dynamic> toJson() => _$PrivateKeyInfoToJson(this);\n\n  String? get type {\n    final lines = key.split('\\n');\n    if (lines.length < 2) {\n      return null;\n    }\n    final firstLine = lines[0];\n    final splited = firstLine.split(RegExp(r'\\s+'));\n    if (splited.length < 2) {\n      return null;\n    }\n    return splited[1];\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/private_key_info.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'private_key_info.dart';\n\n// **************************************************************************\n// JsonSerializableGenerator\n// **************************************************************************\n\nPrivateKeyInfo _$PrivateKeyInfoFromJson(Map<String, dynamic> json) =>\n    PrivateKeyInfo(\n      id: json['id'] as String,\n      key: json['private_key'] as String,\n    );\n\nMap<String, dynamic> _$PrivateKeyInfoToJson(PrivateKeyInfo instance) =>\n    <String, dynamic>{'id': instance.id, 'private_key': instance.key};\n"
  },
  {
    "path": "lib/data/model/server/proc.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\n\nimport 'package:server_box/data/res/misc.dart';\n\nclass _ProcValIdxMap {\n  final int pid;\n  final int? user;\n  final int? cpu;\n  final int? mem;\n  final int? vsz;\n  final int? rss;\n  final int? tty;\n  final int? stat;\n  final int? start;\n  final int? time;\n  final int command;\n\n  const _ProcValIdxMap({\n    required this.pid,\n    this.user,\n    this.cpu,\n    this.mem,\n    this.vsz,\n    this.rss,\n    this.tty,\n    this.stat,\n    this.start,\n    this.time,\n    required this.command,\n  });\n}\n\n/// Some field can be null due to incompatible format on `BSD` and `Alpine`\nclass Proc {\n  final String? user;\n  final int pid;\n  final double? cpu;\n  final double? mem;\n  final String? vsz;\n  final String? rss;\n  final String? tty;\n  final String? stat;\n  final String? start;\n  final String? time;\n  final String command;\n\n  const Proc({\n    this.user,\n    required this.pid,\n    this.cpu,\n    this.mem,\n    this.vsz,\n    this.rss,\n    this.tty,\n    this.stat,\n    this.start,\n    this.time,\n    required this.command,\n  });\n\n  factory Proc._parse(String raw, _ProcValIdxMap map) {\n    final parts = raw.split(RegExp(r'\\s+'));\n    return Proc(\n      user: map.user == null ? null : parts[map.user!],\n      pid: int.parse(parts[map.pid]),\n      cpu: map.cpu == null ? null : double.parse(parts[map.cpu!]),\n      mem: map.mem == null ? null : double.parse(parts[map.mem!]),\n      vsz: map.vsz == null ? null : parts[map.vsz!],\n      rss: map.rss == null ? null : parts[map.rss!],\n      tty: map.tty == null ? null : parts[map.tty!],\n      stat: map.stat == null ? null : parts[map.stat!],\n      start: map.start == null ? null : parts[map.start!],\n      time: map.time == null ? null : parts[map.time!],\n      command: parts.sublist(map.command).join(' '),\n    );\n  }\n\n  Map toJson() {\n    return {\n      'user': user,\n      'pid': pid,\n      'cpu': cpu,\n      'mem': mem,\n      'vsz': vsz,\n      'rss': rss,\n      'tty': tty,\n      'stat': stat,\n      'start': start,\n      'time': time,\n      'command': command,\n    };\n  }\n\n  @override\n  String toString() {\n    return Miscs.jsonEncoder.convert(toJson());\n  }\n\n  String get binary {\n    final parts = command.trim().split(' ').where((e) => e.isNotEmpty).toList();\n    return parts.isNotEmpty ? parts[0] : '';\n  }\n}\n\n// `ps -aux` result\nclass PsResult {\n  final List<Proc> procs;\n  final String? error;\n\n  const PsResult({required this.procs, this.error});\n\n  factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {\n    final lines = raw.split('\\n').map((e) => e.trim()).toList();\n    if (lines.isEmpty) return const PsResult(procs: [], error: null);\n\n    final header = lines[0];\n    final parts = header.split(RegExp(r'\\s+'));\n    parts.removeWhere((element) => element.isEmpty);\n    final map = _ProcValIdxMap(\n      pid: parts.indexOfOrNull('PID')!,\n      user: parts.indexOfOrNull('USER'),\n      cpu: parts.indexOfOrNull('%CPU'),\n      mem: parts.indexOfOrNull('%MEM'),\n      vsz: parts.indexOfOrNull('VSZ'),\n      rss: parts.indexOfOrNull('RSS'),\n      tty: parts.indexOfOrNull('TTY'),\n      stat: parts.indexOfOrNull('STAT'),\n      start: parts.indexOfOrNull('START'),\n      time: parts.indexOfOrNull('TIME'),\n      command: parts.indexOfOrNull('COMMAND') ?? parts.indexOfOrNull('CMD')!,\n    );\n\n    final procs = <Proc>[];\n    final errs = <String>[];\n    for (var i = 1; i < lines.length; i++) {\n      final line = lines[i];\n      if (line.isEmpty) continue;\n      try {\n        procs.add(Proc._parse(line, map));\n      } catch (e, trace) {\n        errs.add('$line: $e');\n        Loggers.app.warning('Process failed', e, trace);\n      }\n    }\n\n    switch (sort) {\n      case ProcSortMode.cpu:\n        procs.sort((a, b) => b.cpu?.compareTo(a.cpu ?? 0) ?? 0);\n        break;\n      case ProcSortMode.mem:\n        procs.sort((a, b) => b.mem?.compareTo(a.mem ?? 0) ?? 0);\n        break;\n      case ProcSortMode.pid:\n        procs.sort((a, b) => a.pid.compareTo(b.pid));\n        break;\n      case ProcSortMode.user:\n        procs.sort((a, b) => a.user?.compareTo(b.user ?? '') ?? 0);\n        break;\n      case ProcSortMode.name:\n        procs.sort((a, b) => a.binary.compareTo(b.binary));\n        break;\n    }\n    return PsResult(procs: procs, error: errs.isEmpty ? null : errs.join('\\n'));\n  }\n}\n\nenum ProcSortMode { cpu, mem, pid, user, name }\n\nextension _StrIndex on List<String> {\n  int? indexOfOrNull(String val) {\n    final idx = indexOf(val);\n    return idx == -1 ? null : idx;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/pve.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\n\nenum PveResType {\n  lxc,\n  qemu,\n  node,\n  storage,\n  sdn;\n\n  static PveResType? fromString(String type) => switch (type.toLowerCase()) {\n    'lxc' => PveResType.lxc,\n    'qemu' => PveResType.qemu,\n    'node' => PveResType.node,\n    'storage' => PveResType.storage,\n    'sdn' => PveResType.sdn,\n    _ => null,\n  };\n\n  String get toStr => switch (this) {\n    PveResType.node => libL10n.node,\n    PveResType.qemu => 'QEMU',\n    PveResType.lxc => 'LXC',\n    PveResType.storage => libL10n.storage,\n    PveResType.sdn => 'SDN',\n  };\n}\n\nsealed class PveResIface {\n  String get id;\n  String get status;\n  PveResType get type;\n\n  static PveResIface? fromJson(Map<String, dynamic> json) {\n    final type = PveResType.fromString(json['type']);\n    if (type == null) return null;\n    switch (type) {\n      case PveResType.lxc:\n        return PveLxc.fromJson(json);\n      case PveResType.qemu:\n        return PveQemu.fromJson(json);\n      case PveResType.node:\n        return PveNode.fromJson(json);\n      case PveResType.storage:\n        return PveStorage.fromJson(json);\n      case PveResType.sdn:\n        return PveSdn.fromJson(json);\n    }\n  }\n}\n\nabstract interface class PveCtrlIface {\n  String get node;\n  String get id;\n  bool get available;\n  String get summary;\n  String get name;\n}\n\nfinal class PveLxc extends PveResIface implements PveCtrlIface {\n  @override\n  final String id;\n  @override\n  final PveResType type;\n  final int vmid;\n  @override\n  final String node;\n  @override\n  final String name;\n  @override\n  final String status;\n  final int uptime;\n  final int mem;\n  final int maxmem;\n  final double cpu;\n  final int maxcpu;\n  final int disk;\n  final int maxdisk;\n  final int diskread;\n  final int diskwrite;\n  final int netin;\n  final int netout;\n\n  PveLxc({\n    required this.id,\n    required this.type,\n    required this.vmid,\n    required this.node,\n    required this.name,\n    required this.status,\n    required this.uptime,\n    required this.mem,\n    required this.maxmem,\n    required this.cpu,\n    required this.maxcpu,\n    required this.disk,\n    required this.maxdisk,\n    required this.diskread,\n    required this.diskwrite,\n    required this.netin,\n    required this.netout,\n  });\n\n  static PveLxc fromJson(Map<String, dynamic> json) {\n    return PveLxc(\n      id: json['id'],\n      type: PveResType.lxc,\n      vmid: json['vmid'],\n      node: json['node'],\n      name: json['name'],\n      status: json['status'],\n      uptime: json['uptime'],\n      mem: json['mem'],\n      maxmem: json['maxmem'],\n      cpu: (json['cpu'] as num).toDouble(),\n      maxcpu: json['maxcpu'],\n      disk: json['disk'],\n      maxdisk: json['maxdisk'],\n      diskread: json['diskread'],\n      diskwrite: json['diskwrite'],\n      netin: json['netin'],\n      netout: json['netout'],\n    );\n  }\n\n  @override\n  bool get available => status == 'running';\n\n  @override\n  String get summary {\n    if (available) {\n      return uptime.secondsToDuration().toAgoStr;\n    }\n    return libL10n.stopped;\n  }\n}\n\nfinal class PveQemu extends PveResIface implements PveCtrlIface {\n  @override\n  final String id;\n  @override\n  final PveResType type;\n  final int vmid;\n  @override\n  final String node;\n  @override\n  final String name;\n  @override\n  final String status;\n  final int uptime;\n  final int mem;\n  final int maxmem;\n  final double cpu;\n  final int maxcpu;\n  final int disk;\n  final int maxdisk;\n  final int diskread;\n  final int diskwrite;\n  final int netin;\n  final int netout;\n\n  PveQemu({\n    required this.id,\n    required this.type,\n    required this.vmid,\n    required this.node,\n    required this.name,\n    required this.status,\n    required this.uptime,\n    required this.mem,\n    required this.maxmem,\n    required this.cpu,\n    required this.maxcpu,\n    required this.disk,\n    required this.maxdisk,\n    required this.diskread,\n    required this.diskwrite,\n    required this.netin,\n    required this.netout,\n  });\n\n  static PveQemu fromJson(Map<String, dynamic> json) {\n    return PveQemu(\n      id: json['id'],\n      type: PveResType.qemu,\n      vmid: json['vmid'],\n      node: json['node'],\n      name: json['name'],\n      status: json['status'],\n      uptime: json['uptime'],\n      mem: json['mem'],\n      maxmem: json['maxmem'],\n      cpu: (json['cpu'] as num).toDouble(),\n      maxcpu: json['maxcpu'],\n      disk: json['disk'],\n      maxdisk: json['maxdisk'],\n      diskread: json['diskread'],\n      diskwrite: json['diskwrite'],\n      netin: json['netin'],\n      netout: json['netout'],\n    );\n  }\n\n  @override\n  bool get available => status == 'running';\n\n  @override\n  String get summary {\n    if (available) {\n      return uptime.secondsToDuration().toAgoStr;\n    }\n    return libL10n.stopped;\n  }\n}\n\nfinal class PveNode extends PveResIface {\n  @override\n  final String id;\n  @override\n  final PveResType type;\n  final String node;\n  @override\n  final String status;\n  final int uptime;\n  final int mem;\n  final int maxmem;\n  final double cpu;\n  final int maxcpu;\n\n  PveNode({\n    required this.id,\n    required this.type,\n    required this.node,\n    required this.status,\n    required this.uptime,\n    required this.mem,\n    required this.maxmem,\n    required this.cpu,\n    required this.maxcpu,\n  });\n\n  static PveNode fromJson(Map<String, dynamic> json) {\n    return PveNode(\n      id: json['id'],\n      type: PveResType.node,\n      node: json['node'],\n      status: json['status'],\n      uptime: json['uptime'],\n      mem: json['mem'],\n      maxmem: json['maxmem'],\n      cpu: (json['cpu'] as num).toDouble(),\n      maxcpu: json['maxcpu'],\n    );\n  }\n\n  bool get isRunning => status == 'online';\n\n  String get topRight {\n    if (isRunning) {\n      return uptime.secondsToDuration().toAgoStr;\n    }\n    return libL10n.stopped;\n  }\n}\n\nfinal class PveStorage extends PveResIface implements PveCtrlIface {\n  @override\n  final String id;\n  @override\n  final PveResType type;\n  final String storage;\n  @override\n  final String node;\n  @override\n  final String status;\n  final String plugintype;\n  final String content;\n  final int shared;\n  final int disk;\n  final int maxdisk;\n\n  PveStorage({\n    required this.id,\n    required this.type,\n    required this.storage,\n    required this.node,\n    required this.status,\n    required this.plugintype,\n    required this.content,\n    required this.shared,\n    required this.disk,\n    required this.maxdisk,\n  });\n\n  static PveStorage fromJson(Map<String, dynamic> json) {\n    return PveStorage(\n      id: json['id'],\n      type: PveResType.storage,\n      storage: json['storage'],\n      node: json['node'],\n      status: json['status'],\n      plugintype: json['plugintype'],\n      content: json['content'],\n      shared: json['shared'],\n      disk: json['disk'],\n      maxdisk: json['maxdisk'],\n    );\n  }\n\n  @override\n  bool get available => status == 'available';\n\n  @override\n  String get name => storage;\n\n  @override\n  String get summary {\n    if (available) {\n      return '${l10n.used}: ${disk.bytes2Str} / ${libL10n.total}: ${maxdisk.bytes2Str}';\n    }\n    return libL10n.notAvailable;\n  }\n}\n\nfinal class PveSdn extends PveResIface implements PveCtrlIface {\n  @override\n  final String id;\n  @override\n  final PveResType type;\n  final String sdn;\n  @override\n  final String node;\n  @override\n  final String status;\n\n  PveSdn({required this.id, required this.type, required this.sdn, required this.node, required this.status});\n\n  static PveSdn fromJson(Map<String, dynamic> json) {\n    return PveSdn(\n      id: json['id'],\n      type: PveResType.sdn,\n      sdn: json['sdn'],\n      node: json['node'],\n      status: json['status'],\n    );\n  }\n\n  @override\n  bool get available => status == 'ok';\n\n  @override\n  String get name => sdn;\n\n  @override\n  String get summary => available ? status : libL10n.notAvailable;\n}\n\nfinal class PveRes {\n  final List<PveNode> nodes;\n  final List<PveQemu> qemus;\n  final List<PveLxc> lxcs;\n  final List<PveStorage> storages;\n  final List<PveSdn> sdns;\n\n  const PveRes({\n    required this.nodes,\n    required this.qemus,\n    required this.lxcs,\n    required this.storages,\n    required this.sdns,\n  });\n\n  bool get onlyOneNode => nodes.length == 1;\n\n  int get length => qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;\n\n  PveResIface operator [](int index) {\n    if (index < nodes.length) {\n      return nodes[index];\n    }\n    index -= nodes.length;\n    if (index < qemus.length) {\n      return qemus[index];\n    }\n    index -= qemus.length;\n    if (index < lxcs.length) {\n      return lxcs[index];\n    }\n    index -= lxcs.length;\n    if (index < storages.length) {\n      return storages[index];\n    }\n    index -= storages.length;\n    return sdns[index];\n  }\n\n  static Future<PveRes> parse((List list, PveRes? old) val) async {\n    final (list, old) = val;\n    final items = list.map((e) => PveResIface.fromJson(e)).toList();\n    final List<PveQemu> qemus = [];\n    final List<PveLxc> lxcs = [];\n    final List<PveNode> nodes = [];\n    final List<PveStorage> storages = [];\n    final List<PveSdn> sdns = [];\n    for (final item in items) {\n      if (item == null) continue;\n      switch (item.type) {\n        case PveResType.lxc:\n          lxcs.add(item as PveLxc);\n          break;\n        case PveResType.qemu:\n          qemus.add(item as PveQemu);\n          break;\n        case PveResType.node:\n          nodes.add(item as PveNode);\n          break;\n        case PveResType.storage:\n          storages.add(item as PveStorage);\n          break;\n        case PveResType.sdn:\n          sdns.add(item as PveSdn);\n          break;\n      }\n    }\n\n    if (old != null) {\n      qemus.reorder(order: old.qemus.map((e) => e.id).toList(), finder: (e, s) => e.id == s);\n      lxcs.reorder(order: old.lxcs.map((e) => e.id).toList(), finder: (e, s) => e.id == s);\n      nodes.reorder(order: old.nodes.map((e) => e.id).toList(), finder: (e, s) => e.id == s);\n      storages.reorder(order: old.storages.map((e) => e.id).toList(), finder: (e, s) => e.id == s);\n      sdns.reorder(order: old.sdns.map((e) => e.id).toList(), finder: (e, s) => e.id == s);\n    }\n\n    return PveRes(qemus: qemus, lxcs: lxcs, nodes: nodes, storages: storages, sdns: sdns);\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/sensors.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\n\nfinal class SensorAdaptor {\n  final String raw;\n\n  const SensorAdaptor(this.raw);\n\n  static const acpiRaw = 'ACPI interface';\n  static const pciRaw = 'PCI adapter';\n  static const virtualRaw = 'Virtual device';\n  static const isaRaw = 'ISA adapter';\n  static const acpi = SensorAdaptor(acpiRaw);\n  static const pci = SensorAdaptor(pciRaw);\n  static const virtual = SensorAdaptor(virtualRaw);\n  static const isa = SensorAdaptor(isaRaw);\n\n  static SensorAdaptor parse(String raw) => switch (raw) {\n    acpiRaw => acpi,\n    pciRaw => pci,\n    virtualRaw => virtual,\n    isaRaw => isa,\n    _ => SensorAdaptor(raw),\n  };\n}\n\nfinal class SensorItem {\n  final String device;\n  final SensorAdaptor adapter;\n  final Map<String, String> details;\n\n  const SensorItem({required this.device, required this.adapter, required this.details});\n\n  String get toMarkdown {\n    final sb = StringBuffer();\n    sb.writeln('| ${libL10n.name} | ${libL10n.content} |');\n    sb.writeln('| --- | --- |');\n    for (final entry in details.entries) {\n      sb.writeln('| ${entry.key} | ${entry.value} |');\n    }\n    return sb.toString();\n  }\n\n  String? get summary {\n    return details.values.firstOrNull;\n  }\n\n  static List<SensorItem> parse(String raw) {\n    final eachSensorLines = <List<String>>[[]];\n    final lines = raw.split('\\n');\n    var emptyLinesCount = 0;\n    for (var i = 0; i < lines.length; i++) {\n      final line = lines[i];\n      if (line.isEmpty) {\n        eachSensorLines.add([]);\n        emptyLinesCount++;\n        continue;\n      }\n      eachSensorLines.last.add(line);\n    }\n\n    if (emptyLinesCount + 1 != eachSensorLines.length) {\n      Loggers.app.warning('Empty lines count not match');\n    }\n\n    final sensors = <SensorItem>[];\n    for (final sensorLines in eachSensorLines) {\n      // At least 3 lines: [device, adapter, temp]\n      final len = sensorLines.length;\n      if (len < 3) continue;\n      final device = sensorLines.first;\n      final adapter = SensorAdaptor.parse(sensorLines[1].split(':').last.trim());\n\n      final details = <String, String>{};\n      for (var idx = 2; idx < len; idx++) {\n        final part = sensorLines[idx];\n        final detailParts = part.split(':');\n        if (detailParts.length < 2) continue;\n        final key = detailParts[0].trim();\n        final value = detailParts[1].trim();\n        details[key] = value;\n      }\n      sensors.add(SensorItem(device: device, adapter: adapter, details: details));\n    }\n\n    return sensors;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/server.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/app/scripts/cmd_types.dart';\nimport 'package:server_box/data/model/server/amd.dart';\nimport 'package:server_box/data/model/server/battery.dart';\nimport 'package:server_box/data/model/server/conn.dart';\nimport 'package:server_box/data/model/server/cpu.dart';\nimport 'package:server_box/data/model/server/disk.dart';\nimport 'package:server_box/data/model/server/disk_smart.dart';\nimport 'package:server_box/data/model/server/memory.dart';\nimport 'package:server_box/data/model/server/net_speed.dart';\nimport 'package:server_box/data/model/server/nvdia.dart';\nimport 'package:server_box/data/model/server/sensors.dart';\nimport 'package:server_box/data/model/server/system.dart';\nimport 'package:server_box/data/model/server/temp.dart';\n\nclass ServerStatus {\n  Cpus cpu;\n  Memory mem;\n  Swap swap;\n  List<Disk> disk;\n  Conn tcp;\n  NetSpeed netSpeed;\n  Temperatures temps;\n  SystemType system;\n  Err? err;\n  DiskIO diskIO;\n  List<DiskSmart> diskSmart;\n  List<NvidiaSmiItem>? nvidia;\n  List<AmdSmiItem>? amd;\n  final List<Battery> batteries = [];\n  final Map<StatusCmdType, String> more = {};\n  final List<SensorItem> sensors = [];\n  DiskUsage? diskUsage;\n  final Map<String, String> customCmds = {};\n\n  ServerStatus({\n    required this.cpu,\n    required this.mem,\n    required this.disk,\n    required this.tcp,\n    required this.netSpeed,\n    required this.swap,\n    required this.temps,\n    required this.system,\n    required this.diskIO,\n    this.diskSmart = const [],\n    this.err,\n    this.nvidia,\n    this.diskUsage,\n  });\n}\n\nenum ServerConn {\n  failed,\n  disconnected,\n  connecting,\n\n  /// Connected to server\n  connected,\n\n  /// Status parsing\n  loading,\n\n  /// Status parsing finished\n  finished;\n\n  bool operator <(ServerConn other) => index < other.index;\n}\n"
  },
  {
    "path": "lib/data/model/server/server_private_info.dart",
    "content": "import 'dart:convert';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:server_box/data/model/app/error.dart';\nimport 'package:server_box/data/model/server/custom.dart';\nimport 'package:server_box/data/model/server/system.dart';\nimport 'package:server_box/data/model/server/wol_cfg.dart';\nimport 'package:server_box/data/store/server.dart';\n\npart 'server_private_info.freezed.dart';\npart 'server_private_info.g.dart';\n\n/// In the first version, it's called `ServerPrivateInfo` which was designed to\n/// store the private information of a server.\n///\n/// Some params named as `spi` in the codebase which is the abbreviation of `ServerPrivateInfo`.\n///\n/// Nowaday, more fields are added to this class, and it's renamed to `Spi`.\n@freezed\nabstract class Spi with _$Spi {\n  const Spi._();\n\n  @JsonSerializable(includeIfNull: false)\n  const factory Spi({\n    required String name,\n    required String ip,\n    required int port,\n    required String user,\n    String? pwd,\n\n    /// [id] of private key\n    @JsonKey(name: 'pubKeyId') String? keyId,\n    List<String>? tags,\n    String? alterUrl,\n    @Default(true) bool autoConnect,\n\n    /// [id] of the jump server\n    String? jumpId,\n    ServerCustom? custom,\n    WakeOnLanCfg? wolCfg,\n\n    /// It only applies to SSH terminal.\n    Map<String, String>? envs,\n    @Default('') @JsonKey(fromJson: Spi.parseId) String id,\n\n    /// Custom system type (unix or windows). If set, skip auto-detection.\n    @JsonKey(includeIfNull: false) SystemType? customSystemType,\n\n    /// Disabled command types for this server\n    @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes,\n  }) = _Spi;\n\n  factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);\n\n  @override\n  String toString() => 'Spi<$oldId>';\n\n  /// Parse the [id], if it's null or empty, generate a new one.\n  static String parseId(Object? id) {\n    if (id == null || id is! String || id.isEmpty) return ShortId.generate();\n    return id;\n  }\n}\n\nextension Spix on Spi {\n  /// After upgrading to >= 1155, this field is only recommended to be used\n  /// for displaying the server name.\n  String get oldId => '$user@$ip:$port';\n\n  /// Save the [Spi] to the local storage.\n  void save() => ServerStore.instance.put(this);\n\n  /// Migrate the [oldId] to the new generated [id] by [ShortId.generate].\n  ///\n  /// Returns:\n  /// - `null` if the [id] is not empty.\n  /// - The new [id] if the [id] is empty.\n  String? migrateId() {\n    if (id.isNotEmpty) return null;\n    ServerStore.instance.delete(oldId);\n    final newSpi = copyWith(id: ShortId.generate());\n    newSpi.save();\n    return newSpi.id;\n  }\n\n  /// Json encode to string.\n  String toJsonString() => json.encode(toJson());\n\n  /// Returns true if the connection info is the same as [other].\n  bool isSameAs(Spi other) {\n    return user == other.user &&\n        ip == other.ip &&\n        port == other.port &&\n        pwd == other.pwd &&\n        keyId == other.keyId &&\n        jumpId == other.jumpId;\n  }\n\n  /// Returns true if the connection should be re-established.\n  bool shouldReconnect(Spi old) {\n    return !isSameAs(old) || alterUrl != old.alterUrl || custom?.cmds != old.custom?.cmds;\n  }\n\n  /// Parse the [alterUrl] to (ip, user, port).\n  (String ip, String usr, int port) parseAlterUrl() {\n    if (alterUrl == null) {\n      throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');\n    }\n    final splited = alterUrl!.split('@');\n    if (splited.length != 2) {\n      throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no @');\n    }\n    final usr = splited[0];\n    final idx = splited[1].lastIndexOf(':');\n    if (idx == -1) {\n      throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no :');\n    }\n    final ip_ = splited[1].substring(0, idx);\n    final port_ = int.tryParse(splited[1].substring(idx + 1));\n    if (port_ == null || port_ <= 0 || port_ > 65535) {\n      throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error');\n    }\n    return (ip_, usr, port_);\n  }\n\n  /// Just for showing the struct of the class.\n  ///\n  /// **NOT** the default value.\n  static final example = Spi(\n    name: 'name',\n    ip: 'ip',\n    port: 22,\n    user: 'root',\n    pwd: 'pwd',\n    keyId: 'private_key_id',\n    tags: ['tag1', 'tag2'],\n    alterUrl: 'user@ip:port',\n    autoConnect: true,\n    jumpId: 'jump_server_id',\n    custom: ServerCustom(\n      pveAddr: 'http://localhost:8006',\n      pveIgnoreCert: false,\n      cmds: {'echo': 'echo hello'},\n      preferTempDev: 'nvme-pci-0400',\n      logoUrl: 'https://example.com/logo.png',\n    ),\n    id: 'id',\n  );\n\n  /// Returns true if the user is 'root'.\n  bool get isRoot => user == 'root';\n}\n"
  },
  {
    "path": "lib/data/model/server/server_private_info.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'server_private_info.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n\n/// @nodoc\nmixin _$Spi {\n\n String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key\n@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server\n String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.\n Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.\n@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server\n@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;\n/// Create a copy of Spi\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity);\n\n  /// Serializes this Spi to a JSON map.\n  Map<String, dynamic> toJson();\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));\n\n\n\n}\n\n/// @nodoc\nabstract mixin class $SpiCopyWith<$Res>  {\n  factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;\n@useResult\n$Res call({\n String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes\n});\n\n\n\n\n}\n/// @nodoc\nclass _$SpiCopyWithImpl<$Res>\n    implements $SpiCopyWith<$Res> {\n  _$SpiCopyWithImpl(this._self, this._then);\n\n  final Spi _self;\n  final $Res Function(Spi) _then;\n\n/// Create a copy of Spi\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {\n  return _then(_self.copyWith(\nname: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable\nas String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable\nas String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable\nas int,user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable\nas String,pwd: freezed == pwd ? _self.pwd : pwd // ignore: cast_nullable_to_non_nullable\nas String?,keyId: freezed == keyId ? _self.keyId : keyId // ignore: cast_nullable_to_non_nullable\nas String?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable\nas List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable\nas String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable\nas bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable\nas String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable\nas ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable\nas WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable\nas Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable\nas String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable\nas SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self.disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable\nas List<String>?,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [Spi].\nextension SpiPatterns on Spi {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Spi value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _Spi() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Spi value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _Spi():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Spi value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _Spi() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name,  String ip,  int port,  String user,  String? pwd, @JsonKey(name: 'pubKeyId')  String? keyId,  List<String>? tags,  String? alterUrl,  bool autoConnect,  String? jumpId,  ServerCustom? custom,  WakeOnLanCfg? wolCfg,  Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId)  String id, @JsonKey(includeIfNull: false)  SystemType? customSystemType, @JsonKey(includeIfNull: false)  List<String>? disabledCmdTypes)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _Spi() when $default != null:\nreturn $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name,  String ip,  int port,  String user,  String? pwd, @JsonKey(name: 'pubKeyId')  String? keyId,  List<String>? tags,  String? alterUrl,  bool autoConnect,  String? jumpId,  ServerCustom? custom,  WakeOnLanCfg? wolCfg,  Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId)  String id, @JsonKey(includeIfNull: false)  SystemType? customSystemType, @JsonKey(includeIfNull: false)  List<String>? disabledCmdTypes)  $default,) {final _that = this;\nswitch (_that) {\ncase _Spi():\nreturn $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name,  String ip,  int port,  String user,  String? pwd, @JsonKey(name: 'pubKeyId')  String? keyId,  List<String>? tags,  String? alterUrl,  bool autoConnect,  String? jumpId,  ServerCustom? custom,  WakeOnLanCfg? wolCfg,  Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId)  String id, @JsonKey(includeIfNull: false)  SystemType? customSystemType, @JsonKey(includeIfNull: false)  List<String>? disabledCmdTypes)?  $default,) {final _that = this;\nswitch (_that) {\ncase _Spi() when $default != null:\nreturn $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n@JsonSerializable(includeIfNull: false)\nclass _Spi extends Spi {\n  const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final  List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final  Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final  List<String>? disabledCmdTypes}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();\n  factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);\n\n@override final  String name;\n@override final  String ip;\n@override final  int port;\n@override final  String user;\n@override final  String? pwd;\n/// [id] of private key\n@override@JsonKey(name: 'pubKeyId') final  String? keyId;\n final  List<String>? _tags;\n@override List<String>? get tags {\n  final value = _tags;\n  if (value == null) return null;\n  if (_tags is EqualUnmodifiableListView) return _tags;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(value);\n}\n\n@override final  String? alterUrl;\n@override@JsonKey() final  bool autoConnect;\n/// [id] of the jump server\n@override final  String? jumpId;\n@override final  ServerCustom? custom;\n@override final  WakeOnLanCfg? wolCfg;\n/// It only applies to SSH terminal.\n final  Map<String, String>? _envs;\n/// It only applies to SSH terminal.\n@override Map<String, String>? get envs {\n  final value = _envs;\n  if (value == null) return null;\n  if (_envs is EqualUnmodifiableMapView) return _envs;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableMapView(value);\n}\n\n@override@JsonKey(fromJson: Spi.parseId) final  String id;\n/// Custom system type (unix or windows). If set, skip auto-detection.\n@override@JsonKey(includeIfNull: false) final  SystemType? customSystemType;\n/// Disabled command types for this server\n final  List<String>? _disabledCmdTypes;\n/// Disabled command types for this server\n@override@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes {\n  final value = _disabledCmdTypes;\n  if (value == null) return null;\n  if (_disabledCmdTypes is EqualUnmodifiableListView) return _disabledCmdTypes;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(value);\n}\n\n\n/// Create a copy of Spi\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$SpiCopyWith<_Spi> get copyWith => __$SpiCopyWithImpl<_Spi>(this, _$identity);\n\n@override\nMap<String, dynamic> toJson() {\n  return _$SpiToJson(this, );\n}\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));\n\n\n\n}\n\n/// @nodoc\nabstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {\n  factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;\n@override @useResult\n$Res call({\n String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes\n});\n\n\n\n\n}\n/// @nodoc\nclass __$SpiCopyWithImpl<$Res>\n    implements _$SpiCopyWith<$Res> {\n  __$SpiCopyWithImpl(this._self, this._then);\n\n  final _Spi _self;\n  final $Res Function(_Spi) _then;\n\n/// Create a copy of Spi\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {\n  return _then(_Spi(\nname: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable\nas String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable\nas String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable\nas int,user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable\nas String,pwd: freezed == pwd ? _self.pwd : pwd // ignore: cast_nullable_to_non_nullable\nas String?,keyId: freezed == keyId ? _self.keyId : keyId // ignore: cast_nullable_to_non_nullable\nas String?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable\nas List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable\nas String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable\nas bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable\nas String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable\nas ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable\nas WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable\nas Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable\nas String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable\nas SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self._disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable\nas List<String>?,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/model/server/server_private_info.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'server_private_info.dart';\n\n// **************************************************************************\n// JsonSerializableGenerator\n// **************************************************************************\n\n_Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(\n  name: json['name'] as String,\n  ip: json['ip'] as String,\n  port: (json['port'] as num).toInt(),\n  user: json['user'] as String,\n  pwd: json['pwd'] as String?,\n  keyId: json['pubKeyId'] as String?,\n  tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),\n  alterUrl: json['alterUrl'] as String?,\n  autoConnect: json['autoConnect'] as bool? ?? true,\n  jumpId: json['jumpId'] as String?,\n  custom: json['custom'] == null\n      ? null\n      : ServerCustom.fromJson(json['custom'] as Map<String, dynamic>),\n  wolCfg: json['wolCfg'] == null\n      ? null\n      : WakeOnLanCfg.fromJson(json['wolCfg'] as Map<String, dynamic>),\n  envs: (json['envs'] as Map<String, dynamic>?)?.map(\n    (k, e) => MapEntry(k, e as String),\n  ),\n  id: json['id'] == null ? '' : Spi.parseId(json['id']),\n  customSystemType: $enumDecodeNullable(\n    _$SystemTypeEnumMap,\n    json['customSystemType'],\n  ),\n  disabledCmdTypes: (json['disabledCmdTypes'] as List<dynamic>?)\n      ?.map((e) => e as String)\n      .toList(),\n);\n\nMap<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{\n  'name': instance.name,\n  'ip': instance.ip,\n  'port': instance.port,\n  'user': instance.user,\n  'pwd': ?instance.pwd,\n  'pubKeyId': ?instance.keyId,\n  'tags': ?instance.tags,\n  'alterUrl': ?instance.alterUrl,\n  'autoConnect': instance.autoConnect,\n  'jumpId': ?instance.jumpId,\n  'custom': ?instance.custom,\n  'wolCfg': ?instance.wolCfg,\n  'envs': ?instance.envs,\n  'id': instance.id,\n  'customSystemType': ?_$SystemTypeEnumMap[instance.customSystemType],\n  'disabledCmdTypes': ?instance.disabledCmdTypes,\n};\n\nconst _$SystemTypeEnumMap = {\n  SystemType.linux: 'linux',\n  SystemType.bsd: 'bsd',\n  SystemType.windows: 'windows',\n};\n"
  },
  {
    "path": "lib/data/model/server/server_status_update_req.dart",
    "content": "import 'dart:convert';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/app/scripts/cmd_types.dart';\nimport 'package:server_box/data/model/app/scripts/script_consts.dart';\nimport 'package:server_box/data/model/server/amd.dart';\nimport 'package:server_box/data/model/server/battery.dart';\nimport 'package:server_box/data/model/server/conn.dart';\nimport 'package:server_box/data/model/server/cpu.dart';\nimport 'package:server_box/data/model/server/disk.dart';\nimport 'package:server_box/data/model/server/disk_smart.dart';\nimport 'package:server_box/data/model/server/memory.dart';\nimport 'package:server_box/data/model/server/net_speed.dart';\nimport 'package:server_box/data/model/server/nvdia.dart';\nimport 'package:server_box/data/model/server/sensors.dart';\nimport 'package:server_box/data/model/server/server.dart';\nimport 'package:server_box/data/model/server/system.dart';\nimport 'package:server_box/data/model/server/temp.dart';\nimport 'package:server_box/data/model/server/windows_parser.dart';\n\nclass ServerStatusUpdateReq {\n  final ServerStatus ss;\n  final Map<String, String> parsedOutput;\n  final SystemType system;\n  final Map<String, String> customCmds;\n\n  const ServerStatusUpdateReq({\n    required this.system,\n    required this.ss,\n    required this.parsedOutput,\n    required this.customCmds,\n  });\n}\n\nFuture<ServerStatus> getStatus(ServerStatusUpdateReq req) async {\n  return switch (req.system) {\n    SystemType.linux => _getLinuxStatus(req),\n    SystemType.bsd => _getBsdStatus(req),\n    SystemType.windows => _getWindowsStatus(req),\n  };\n}\n\n// Wrap each operation with a try-catch, so that if one operation fails,\n// the following operations can still be executed.\nFuture<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {\n  final parsedOutput = req.parsedOutput;\n\n  final time =\n      int.tryParse(StatusCmdType.time.findInMap(parsedOutput)) ??\n      DateTime.now().millisecondsSinceEpoch ~/ 1000;\n\n  try {\n    final net = NetSpeed.parse(StatusCmdType.net.findInMap(parsedOutput), time);\n    req.ss.netSpeed.update(net);\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    final sys = _parseSysVer(StatusCmdType.sys.findInMap(parsedOutput));\n    if (sys != null) {\n      req.ss.more[StatusCmdType.sys] = sys;\n    }\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    final host = _parseHostName(StatusCmdType.host.findInMap(parsedOutput));\n    if (host != null) {\n      req.ss.more[StatusCmdType.host] = host;\n    }\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    final cpus = SingleCpuCore.parse(StatusCmdType.cpu.findInMap(parsedOutput));\n    req.ss.cpu.update(cpus);\n    final brand = CpuBrand.parse(StatusCmdType.cpuBrand.findInMap(parsedOutput));\n    req.ss.cpu.brand.clear();\n    req.ss.cpu.brand.addAll(brand);\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.temps.parse(\n      StatusCmdType.tempType.findInMap(parsedOutput),\n      StatusCmdType.tempVal.findInMap(parsedOutput),\n    );\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    final tcp = Conn.parse(StatusCmdType.conn.findInMap(parsedOutput));\n    if (tcp != null) {\n      req.ss.tcp = tcp;\n    }\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.disk = Disk.parse(StatusCmdType.disk.findInMap(parsedOutput));\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.diskUsage = DiskUsage.parse(req.ss.disk);\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.mem = Memory.parse(StatusCmdType.mem.findInMap(parsedOutput));\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    final uptime = _parseUpTime(StatusCmdType.uptime.findInMap(parsedOutput));\n    if (uptime != null) {\n      req.ss.more[StatusCmdType.uptime] = uptime;\n    }\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.swap = Swap.parse(StatusCmdType.mem.findInMap(parsedOutput));\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    final diskio = DiskIO.parse(StatusCmdType.diskio.findInMap(parsedOutput), time);\n    req.ss.diskIO.update(diskio);\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    final smarts = DiskSmart.parse(StatusCmdType.diskSmart.findInMap(parsedOutput));\n    req.ss.diskSmart = smarts;\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.findInMap(parsedOutput));\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.findInMap(parsedOutput));\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    final battery = StatusCmdType.battery.findInMap(parsedOutput);\n\n    /// Only collect li-poly batteries\n    final batteries = Batteries.parse(battery, true);\n    req.ss.batteries.clear();\n    if (batteries.isNotEmpty) {\n      req.ss.batteries.addAll(batteries);\n    }\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    final sensors = SensorItem.parse(StatusCmdType.sensors.findInMap(parsedOutput));\n    if (sensors.isNotEmpty) {\n      req.ss.sensors.clear();\n      req.ss.sensors.addAll(sensors);\n    }\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    for (final entry in req.customCmds.entries) {\n      final key = entry.key;\n      final value = req.parsedOutput[key] ?? '';\n      req.ss.customCmds[key] = value;\n    }\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  return req.ss;\n}\n\n// Same as above, wrap with try-catch\nFuture<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {\n  final parsedOutput = req.parsedOutput;\n\n  try {\n    final time = int.parse(BSDStatusCmdType.time.findInMap(parsedOutput));\n    final net = NetSpeed.parseBsd(BSDStatusCmdType.net.findInMap(parsedOutput), time);\n    req.ss.netSpeed.update(net);\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.more[StatusCmdType.sys] = BSDStatusCmdType.sys.findInMap(parsedOutput);\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.cpu = parseBsdCpu(BSDStatusCmdType.cpu.findInMap(parsedOutput));\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.mem = parseBsdMemory(BSDStatusCmdType.mem.findInMap(parsedOutput));\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    final uptime = _parseUpTime(BSDStatusCmdType.uptime.findInMap(parsedOutput));\n    if (uptime != null) {\n      req.ss.more[StatusCmdType.uptime] = uptime;\n    }\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n\n  try {\n    req.ss.disk = Disk.parse(BSDStatusCmdType.disk.findInMap(parsedOutput));\n  } catch (e, s) {\n    Loggers.app.warning(e, s);\n  }\n  return req.ss;\n}\n\n// raw:\n//  19:39:15 up 61 days, 18:16,  1 user,  load average: 0.00, 0.00, 0.00\n//  19:39:15 up 1 day, 2:34,  1 user,  load average: 0.00, 0.00, 0.00\n//  19:39:15 up 2:34,  1 user,  load average: 0.00, 0.00, 0.00\n//  19:39:15 up 34 min,  1 user,  load average: 0.00, 0.00, 0.00\nString? _parseUpTime(String raw) {\n  final splitedUp = raw.split('up ');\n  if (splitedUp.length == 2) {\n    final uptimePart = splitedUp[1];\n    final splitedComma = uptimePart.split(', ');\n\n    if (splitedComma.isEmpty) return null;\n\n    // Handle different uptime formats\n    final firstPart = splitedComma[0].trim();\n\n    // Case 1: \"61 days\" or \"1 day\" - need to get the time part from next segment\n    if (firstPart.contains('day')) {\n      if (splitedComma.length >= 2) {\n        final timePart = splitedComma[1].trim();\n        // Check if it's in HH:MM format\n        if (timePart.contains(':') && !timePart.contains('user') && !timePart.contains('load')) {\n          return '$firstPart, $timePart';\n        }\n      }\n      return firstPart;\n    }\n\n    // Case 2: \"2:34\" (hours:minutes) - already in good format\n    if (firstPart.contains(':') && !firstPart.contains('user') && !firstPart.contains('load')) {\n      return firstPart;\n    }\n\n    // Case 3: \"34 min\" - already in good format\n    if (firstPart.contains('min')) {\n      return firstPart;\n    }\n\n    // Fallback: return first part\n    return firstPart;\n  }\n  return null;\n}\n\nString? _parseSysVer(String raw) {\n  final s = raw.split('=');\n  if (s.length == 2) {\n    return s[1].replaceAll('\"', '').replaceFirst('\\n', '');\n  }\n  return null;\n}\n\nString? _parseHostName(String raw) {\n  if (raw.isEmpty) return null;\n  if (raw.contains(ScriptConstants.scriptFile)) return null;\n  final trimmed = raw.trim();\n  if (trimmed.isEmpty) return null;\n  return trimmed;\n}\n\n// Windows status parsing implementation\nFuture<ServerStatus> _getWindowsStatus(ServerStatusUpdateReq req) async {\n  final parsedOutput = req.parsedOutput;\n  final time =\n      int.tryParse(WindowsStatusCmdType.time.findInMap(parsedOutput)) ??\n      DateTime.now().millisecondsSinceEpoch ~/ 1000;\n\n  // Parse all different resource types using helper methods\n  _parseWindowsNetworkData(req, parsedOutput, time);\n  _parseWindowsSystemData(req, parsedOutput);\n  _parseWindowsHostData(req, parsedOutput);\n  _parseWindowsCpuData(req, parsedOutput);\n  _parseWindowsMemoryData(req, parsedOutput);\n  _parseWindowsDiskData(req, parsedOutput);\n  _parseWindowsUptimeData(req, parsedOutput);\n  _parseWindowsDiskIOData(req, parsedOutput, time);\n  _parseWindowsConnectionData(req, parsedOutput);\n  _parseWindowsBatteryData(req, parsedOutput);\n  _parseWindowsTemperatureData(req, parsedOutput);\n  _parseWindowsGpuData(req, parsedOutput);\n  WindowsParser.parseCustomCommands(req.ss, req.parsedOutput, req.customCmds);\n\n  return req.ss;\n}\n\n/// Parse Windows network data\nvoid _parseWindowsNetworkData(ServerStatusUpdateReq req, Map<String, String> parsedOutput, int time) {\n  try {\n    final netRaw = WindowsStatusCmdType.net.findInMap(parsedOutput);\n    if (netRaw.isNotEmpty &&\n        netRaw != 'null' &&\n        !netRaw.contains('network_error') &&\n        !netRaw.contains('error') &&\n        !netRaw.contains('Exception')) {\n      final netParts = _parseWindowsNetwork(netRaw, time);\n      if (netParts.isNotEmpty) {\n        req.ss.netSpeed.update(netParts);\n      }\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows network parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows system information\nvoid _parseWindowsSystemData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {\n  try {\n    final sys = WindowsStatusCmdType.sys.findInMap(parsedOutput);\n    if (sys.isNotEmpty) {\n      req.ss.more[StatusCmdType.sys] = sys;\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows system parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows host information\nvoid _parseWindowsHostData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {\n  try {\n    final host = _parseHostName(WindowsStatusCmdType.host.findInMap(parsedOutput));\n    if (host != null) {\n      req.ss.more[StatusCmdType.host] = host;\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows host parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows CPU data and brand information\nvoid _parseWindowsCpuData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {\n  try {\n    // Windows CPU parsing - JSON format from PowerShell\n    final cpuRaw = WindowsStatusCmdType.cpu.findInMap(parsedOutput);\n    if (cpuRaw.isNotEmpty && cpuRaw != 'null' && !cpuRaw.contains('error') && !cpuRaw.contains('Exception')) {\n      final cpuResult = WindowsParser.parseCpu(cpuRaw, req.ss);\n      if (cpuResult.cores.isNotEmpty) {\n        req.ss.cpu.update(cpuResult.cores);\n        final brandRaw = WindowsStatusCmdType.cpuBrand.findInMap(parsedOutput);\n        if (brandRaw.isNotEmpty && brandRaw != 'null') {\n          req.ss.cpu.brand.clear();\n          final brandLines = brandRaw.trim().split('\\n');\n          final uniqueBrands = <String>{};\n          for (final line in brandLines) {\n            final trimmedLine = line.trim();\n            if (trimmedLine.isNotEmpty) {\n              uniqueBrands.add(trimmedLine);\n            }\n          }\n          if (uniqueBrands.isNotEmpty) {\n            final brandName = uniqueBrands.first;\n            req.ss.cpu.brand[brandName] = cpuResult.coreCount;\n          }\n        }\n      }\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows CPU parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows memory data\nvoid _parseWindowsMemoryData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {\n  try {\n    final memRaw = WindowsStatusCmdType.mem.findInMap(parsedOutput);\n    if (memRaw.isNotEmpty && memRaw != 'null' && !memRaw.contains('error') && !memRaw.contains('Exception')) {\n      final memory = WindowsParser.parseMemory(memRaw);\n      if (memory != null) {\n        req.ss.mem = memory;\n      }\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows memory parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows disk data\nvoid _parseWindowsDiskData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {\n  try {\n    final diskRaw = WindowsStatusCmdType.disk.findInMap(parsedOutput);\n    if (diskRaw.isNotEmpty && diskRaw != 'null') {\n      final disks = WindowsParser.parseDisks(diskRaw);\n      req.ss.disk = disks;\n      req.ss.diskUsage = DiskUsage.parse(disks);\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows disk parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows uptime data\nvoid _parseWindowsUptimeData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {\n  try {\n    final uptimeRaw = WindowsStatusCmdType.uptime.findInMap(parsedOutput);\n    if (uptimeRaw.isNotEmpty && uptimeRaw != 'null') {\n      // PowerShell now returns pre-formatted uptime string (e.g., \"28 days, 5:00\" or \"5:00\")\n      // No parsing needed - use it directly\n      final uptime = uptimeRaw.trim();\n      req.ss.more[StatusCmdType.uptime] = uptime;\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows uptime parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows disk I/O data\nvoid _parseWindowsDiskIOData(ServerStatusUpdateReq req, Map<String, String> parsedOutput, int time) {\n  try {\n    final diskIOraw = WindowsStatusCmdType.diskio.findInMap(parsedOutput);\n    if (diskIOraw.isNotEmpty && diskIOraw != 'null') {\n      final diskio = _parseWindowsDiskIO(diskIOraw, time);\n      req.ss.diskIO.update(diskio);\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows disk I/O parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows connection data\nvoid _parseWindowsConnectionData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {\n  try {\n    final connStr = WindowsStatusCmdType.conn.findInMap(parsedOutput);\n    final connCount = int.tryParse(connStr.trim());\n    if (connCount != null) {\n      req.ss.tcp = Conn(maxConn: 0, active: connCount, passive: 0, fail: 0);\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows connection parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows battery data\nvoid _parseWindowsBatteryData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {\n  try {\n    final batteryRaw = WindowsStatusCmdType.battery.findInMap(parsedOutput);\n    if (batteryRaw.isNotEmpty && batteryRaw != 'null') {\n      final batteries = _parseWindowsBatteries(batteryRaw);\n      req.ss.batteries.clear();\n      if (batteries.isNotEmpty) {\n        req.ss.batteries.addAll(batteries);\n      }\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows battery parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows temperature data\nvoid _parseWindowsTemperatureData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {\n  try {\n    final tempRaw = WindowsStatusCmdType.temp.findInMap(parsedOutput);\n    if (tempRaw.isNotEmpty && tempRaw != 'null') {\n      _parseWindowsTemperatures(req.ss.temps, tempRaw);\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Windows temperature parsing failed: $e', s);\n  }\n}\n\n/// Parse Windows GPU data (NVIDIA/AMD)\nvoid _parseWindowsGpuData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {\n  try {\n    req.ss.nvidia = NvidiaSmi.fromXml(WindowsStatusCmdType.nvidia.findInMap(parsedOutput));\n  } catch (e, s) {\n    Loggers.app.warning('Windows NVIDIA GPU parsing failed: $e', s);\n  }\n\n  try {\n    req.ss.amd = AmdSmi.fromJson(WindowsStatusCmdType.amd.findInMap(parsedOutput));\n  } catch (e, s) {\n    Loggers.app.warning('Windows AMD GPU parsing failed: $e', s);\n  }\n}\n\nList<Battery> _parseWindowsBatteries(String raw) {\n  try {\n    final dynamic jsonData = json.decode(raw);\n    final List<Battery> batteries = [];\n\n    final batteryList = jsonData is List ? jsonData : [jsonData];\n\n    for (final batteryData in batteryList) {\n      final chargeRemaining = batteryData['EstimatedChargeRemaining'] as int? ?? 0;\n      final batteryStatus = batteryData['BatteryStatus'] as int? ?? 0;\n\n      // Windows battery status: 1=Other, 2=Unknown, 3=Full, 4=Low,\n      // 5=Critical, 6=Charging, 7=ChargingAndLow, 8=ChargingAndCritical,\n      // 9=Undefined, 10=PartiallyCharged\n      final isCharging = batteryStatus == 6 || batteryStatus == 7 || batteryStatus == 8;\n\n      batteries.add(\n        Battery(\n          name: 'Battery',\n          percent: chargeRemaining,\n          status: isCharging ? BatteryStatus.charging : BatteryStatus.discharging,\n        ),\n      );\n    }\n\n    return batteries;\n  } catch (e) {\n    return [];\n  }\n}\n\nList<NetSpeedPart> _parseWindowsNetwork(String raw, int currentTime) {\n  try {\n    final dynamic jsonData = json.decode(raw);\n    final List<NetSpeedPart> netParts = [];\n\n    if (jsonData is List && jsonData.length >= 2) {\n      var sample1 = jsonData[jsonData.length - 2];\n      var sample2 = jsonData[jsonData.length - 1];\n      if (sample1 is Map && sample1.containsKey('value')) {\n        sample1 = sample1['value'];\n      }\n      if (sample2 is Map && sample2.containsKey('value')) {\n        sample2 = sample2['value'];\n      }\n      if (sample1 is List && sample2 is List && sample1.length == sample2.length) {\n        for (int i = 0; i < sample1.length; i++) {\n          final s1 = sample1[i];\n          final s2 = sample2[i];\n          final name = s1['Name']?.toString() ?? '';\n          if (name.isEmpty || name == '_Total') continue;\n          final rx1 = (s1['BytesReceivedPersec'] as num?)?.toDouble() ?? 0;\n          final rx2 = (s2['BytesReceivedPersec'] as num?)?.toDouble() ?? 0;\n          final tx1 = (s1['BytesSentPersec'] as num?)?.toDouble() ?? 0;\n          final tx2 = (s2['BytesSentPersec'] as num?)?.toDouble() ?? 0;\n          final time1 = (s1['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;\n          final time2 = (s2['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;\n          final timeDelta = (time2 - time1) / 10000000;\n          if (timeDelta <= 0) continue;\n          final rxDelta = rx2 - rx1;\n          final txDelta = tx2 - tx1;\n          if (rxDelta < 0 || txDelta < 0) continue;\n          final rxSpeed = rxDelta / timeDelta;\n          final txSpeed = txDelta / timeDelta;\n          netParts.add(\n            NetSpeedPart(name, BigInt.from(rxSpeed.toInt()), BigInt.from(txSpeed.toInt()), currentTime),\n          );\n        }\n      }\n    }\n\n    return netParts;\n  } catch (e) {\n    return [];\n  }\n}\n\nList<DiskIOPiece> _parseWindowsDiskIO(String raw, int currentTime) {\n  try {\n    final dynamic jsonData = json.decode(raw);\n    final List<DiskIOPiece> diskParts = [];\n\n    if (jsonData is List && jsonData.length >= 2) {\n      var sample1 = jsonData[jsonData.length - 2];\n      var sample2 = jsonData[jsonData.length - 1];\n      if (sample1 is Map && sample1.containsKey('value')) {\n        sample1 = sample1['value'];\n      }\n      if (sample2 is Map && sample2.containsKey('value')) {\n        sample2 = sample2['value'];\n      }\n      if (sample1 is List && sample2 is List && sample1.length == sample2.length) {\n        for (int i = 0; i < sample1.length; i++) {\n          final s1 = sample1[i];\n          final s2 = sample2[i];\n          final name = s1['Name']?.toString() ?? '';\n          if (name.isEmpty || name == '_Total') continue;\n          final read1 = (s1['DiskReadBytesPersec'] as num?)?.toDouble() ?? 0;\n          final read2 = (s2['DiskReadBytesPersec'] as num?)?.toDouble() ?? 0;\n          final write1 = (s1['DiskWriteBytesPersec'] as num?)?.toDouble() ?? 0;\n          final write2 = (s2['DiskWriteBytesPersec'] as num?)?.toDouble() ?? 0;\n          final time1 = (s1['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;\n          final time2 = (s2['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;\n          final timeDelta = (time2 - time1) / 10000000;\n          if (timeDelta <= 0) continue;\n          final readDelta = read2 - read1;\n          final writeDelta = write2 - write1;\n          if (readDelta < 0 || writeDelta < 0) continue;\n          final readSpeed = readDelta / timeDelta;\n          final writeSpeed = writeDelta / timeDelta;\n          final sectorsRead = (readSpeed / 512).round();\n          final sectorsWrite = (writeSpeed / 512).round();\n\n          diskParts.add(\n            DiskIOPiece(\n              dev: name,\n              sectorsRead: sectorsRead,\n              sectorsWrite: sectorsWrite,\n              time: currentTime,\n            ),\n          );\n        }\n      }\n    }\n\n    return diskParts;\n  } catch (e) {\n    return [];\n  }\n}\n\nvoid _parseWindowsTemperatures(Temperatures temps, String raw) {\n  try {\n    // Handle error output\n    if (raw.contains('Error') || raw.contains('Exception') || raw.contains('The term')) {\n      return;\n    }\n\n    final dynamic jsonData = json.decode(raw);\n    final tempList = jsonData is List ? jsonData : [jsonData];\n\n    // Create fake type and value strings that the existing parse method can handle\n    final typeLines = <String>[];\n    final valueLines = <String>[];\n\n    for (int i = 0; i < tempList.length; i++) {\n      final item = tempList[i];\n      final typeName = item['InstanceName']?.toString() ?? 'Unknown';\n      final temperature = item['Temperature'] as num?;\n\n      if (temperature != null) {\n        // Convert to the format expected by the existing parse method\n        typeLines.add('/sys/class/thermal/thermal_zone$i/$typeName');\n        // Convert to millicelsius (multiply by 1000)\n        // as expected by Linux parsing\n        valueLines.add((temperature * 1000).round().toString());\n      }\n    }\n\n    if (typeLines.isNotEmpty && valueLines.isNotEmpty) {\n      temps.parse(typeLines.join('\\n'), valueLines.join('\\n'));\n    }\n  } catch (e, s) {\n    Loggers.app.warning('Failed to parse Windows temperature data', e, s);\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/snippet.dart",
    "content": "import 'dart:async';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:xterm/core.dart';\n\npart 'snippet.g.dart';\npart 'snippet.freezed.dart';\n\n@freezed\nabstract class Snippet with _$Snippet {\n  const factory Snippet({\n    required String name,\n    required String script,\n    List<String>? tags,\n    String? note,\n\n    /// List of server id that this snippet should be auto run on\n    List<String>? autoRunOn,\n  }) = _Snippet;\n\n  factory Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json);\n\n  static const example = Snippet(\n    name: 'example',\n    script: 'echo hello',\n    tags: ['tag'],\n    note: 'note',\n    autoRunOn: ['server_id'],\n  );\n}\n\nextension SnippetX on Snippet {\n  static final fmtFinder = RegExp(r'\\$\\{[^{}]+\\}');\n\n  String fmtWithSpi(Spi spi) {\n    return script.replaceAllMapped(fmtFinder, (match) {\n      final key = match.group(0);\n      final func = fmtArgs[key];\n      if (func != null) return func(spi);\n      // If not found, return the original content for further processing\n      return key ?? '';\n    });\n  }\n\n  Future<void> runInTerm(Terminal terminal, Spi spi, {bool autoEnter = false}) async {\n    final argsFmted = fmtWithSpi(spi);\n    final matches = fmtFinder.allMatches(argsFmted);\n\n    /// There is no [TerminalKey] in the script\n    if (matches.isEmpty) {\n      terminal.textInput(argsFmted);\n      if (autoEnter) terminal.keyInput(TerminalKey.enter);\n      return;\n    }\n\n    // Records all start and end indexes of the matches\n    final (starts, ends) = matches.fold((<int>[], <int>[]), (pre, e) {\n      pre.$1.add(e.start);\n      pre.$2.add(e.end);\n      return pre;\n    });\n\n    // Check all indexes, `(idx + 1).start` must >= `idx.end`\n    for (var i = 0; i < starts.length - 1; i++) {\n      final lastEnd = ends[i];\n      final nextStart = starts[i + 1];\n      if (nextStart < lastEnd) {\n        throw 'Invalid format: $nextStart < $lastEnd';\n      }\n    }\n\n    // Start term input\n    if (starts.first > 0) {\n      terminal.textInput(argsFmted.substring(0, starts.first));\n    }\n\n    // Process matched\n    for (var idx = 0; idx < starts.length; idx++) {\n      final start = starts[idx];\n      final end = ends[idx];\n      final key = argsFmted.substring(start, end).toLowerCase();\n\n      // Special funcs\n      final special = _find(SnippetFuncs.specialCtrl, key);\n      if (special != null) {\n        final raw = key.substring(special.key.length + 1, key.length - 1);\n        await special.value((term: terminal, raw: raw));\n      } else {\n        // Term keys\n        final termKey = _find(fmtTermKeys, key);\n        if (termKey != null) {\n          await _doTermKeys(terminal, termKey, key);\n        } else {\n          // Normal input\n          terminal.textInput(key);\n        }\n      }\n\n      // Text between this and next match\n      if (idx < starts.length - 1) {\n        terminal.textInput(argsFmted.substring(end, starts[idx + 1]));\n      }\n    }\n\n    // End term input\n    if (ends.last < argsFmted.length) {\n      terminal.textInput(argsFmted.substring(ends.last));\n    }\n\n    if (autoEnter) terminal.keyInput(TerminalKey.enter);\n  }\n\n  Future<void> _doTermKeys(Terminal terminal, MapEntry<String, TerminalKey> termKey, String key) async {\n    // if (termKey.value == TerminalKey.enter) {\n    //   terminal.keyInput(TerminalKey.enter);\n    //   return;\n    // }\n\n    final ctrlAlt = switch (termKey.value) {\n      TerminalKey.control => (ctrl: true, alt: false),\n      TerminalKey.alt => (ctrl: false, alt: true),\n      _ => (ctrl: false, alt: false),\n    };\n\n    if (!key.contains('+')) return;\n\n    // `${ctrl+ad}` -> `ctrla + d`\n    final chars = key.substring(termKey.key.length + 1, key.length - 1);\n    if (chars.isEmpty) return;\n    final ok = terminal.charInput(chars.codeUnitAt(0), ctrl: ctrlAlt.ctrl, alt: ctrlAlt.alt);\n    if (!ok) {\n      Loggers.app.warning('Failed to input: $key');\n    }\n\n    terminal.textInput(chars.substring(1));\n  }\n\n  MapEntry<String, T>? _find<T>(Map<String, T> map, String key) {\n    return map.entries.firstWhereOrNull((e) => key.startsWith(e.key));\n  }\n\n  static final fmtArgs = {\n    r'${host}': (Spi spi) => spi.ip,\n    r'${port}': (Spi spi) => spi.port.toString(),\n    r'${user}': (Spi spi) => spi.user,\n    r'${pwd}': (Spi spi) => spi.pwd ?? '',\n    r'${id}': (Spi spi) => spi.id,\n    r'${name}': (Spi spi) => spi.name,\n  };\n\n  /// r'${ctrl+ad}' -> TerminalKey.control, a, d\n  static final fmtTermKeys = {r'${ctrl': TerminalKey.control, r'${alt': TerminalKey.alt};\n}\n\nclass SnippetResult {\n  final String? dest;\n  final String result;\n  final Duration time;\n\n  SnippetResult({required this.dest, required this.result, required this.time});\n}\n\ntypedef SnippetFuncCtx = ({Terminal term, String raw});\n\nabstract final class SnippetFuncs {\n  static final specialCtrl = {\n    // `${sleep 3}` -> sleep 3 seconds\n    r'${sleep': SnippetFuncs.sleep,\n    r'${enter': SnippetFuncs.enter,\n  };\n\n  static const help = {'sleep': 'Sleep for a few seconds', 'enter': 'Enter a few times'};\n\n  static FutureOr<void> sleep(SnippetFuncCtx ctx) async {\n    final seconds = int.tryParse(ctx.raw);\n    if (seconds == null) return;\n    final duration = Duration(seconds: seconds);\n    await Future.delayed(duration);\n  }\n\n  static FutureOr<void> enter(SnippetFuncCtx ctx) async {\n    final times = int.tryParse(ctx.raw) ?? 1;\n    for (var i = 0; i < times; i++) {\n      ctx.term.keyInput(TerminalKey.enter);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/snippet.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'snippet.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n\n/// @nodoc\nmixin _$Snippet {\n\n String get name; String get script; List<String>? get tags; String? get note;/// List of server id that this snippet should be auto run on\n List<String>? get autoRunOn;\n/// Create a copy of Snippet\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$SnippetCopyWith<Snippet> get copyWith => _$SnippetCopyWithImpl<Snippet>(this as Snippet, _$identity);\n\n  /// Serializes this Snippet to a JSON map.\n  Map<String, dynamic> toJson();\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is Snippet&&(identical(other.name, name) || other.name == name)&&(identical(other.script, script) || other.script == script)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.note, note) || other.note == note)&&const DeepCollectionEquality().equals(other.autoRunOn, autoRunOn));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,name,script,const DeepCollectionEquality().hash(tags),note,const DeepCollectionEquality().hash(autoRunOn));\n\n@override\nString toString() {\n  return 'Snippet(name: $name, script: $script, tags: $tags, note: $note, autoRunOn: $autoRunOn)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $SnippetCopyWith<$Res>  {\n  factory $SnippetCopyWith(Snippet value, $Res Function(Snippet) _then) = _$SnippetCopyWithImpl;\n@useResult\n$Res call({\n String name, String script, List<String>? tags, String? note, List<String>? autoRunOn\n});\n\n\n\n\n}\n/// @nodoc\nclass _$SnippetCopyWithImpl<$Res>\n    implements $SnippetCopyWith<$Res> {\n  _$SnippetCopyWithImpl(this._self, this._then);\n\n  final Snippet _self;\n  final $Res Function(Snippet) _then;\n\n/// Create a copy of Snippet\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? script = null,Object? tags = freezed,Object? note = freezed,Object? autoRunOn = freezed,}) {\n  return _then(_self.copyWith(\nname: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable\nas String,script: null == script ? _self.script : script // ignore: cast_nullable_to_non_nullable\nas String,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable\nas List<String>?,note: freezed == note ? _self.note : note // ignore: cast_nullable_to_non_nullable\nas String?,autoRunOn: freezed == autoRunOn ? _self.autoRunOn : autoRunOn // ignore: cast_nullable_to_non_nullable\nas List<String>?,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [Snippet].\nextension SnippetPatterns on Snippet {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Snippet value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _Snippet() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Snippet value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _Snippet():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Snippet value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _Snippet() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name,  String script,  List<String>? tags,  String? note,  List<String>? autoRunOn)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _Snippet() when $default != null:\nreturn $default(_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name,  String script,  List<String>? tags,  String? note,  List<String>? autoRunOn)  $default,) {final _that = this;\nswitch (_that) {\ncase _Snippet():\nreturn $default(_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name,  String script,  List<String>? tags,  String? note,  List<String>? autoRunOn)?  $default,) {final _that = this;\nswitch (_that) {\ncase _Snippet() when $default != null:\nreturn $default(_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n@JsonSerializable()\n\nclass _Snippet implements Snippet {\n  const _Snippet({required this.name, required this.script, final  List<String>? tags, this.note, final  List<String>? autoRunOn}): _tags = tags,_autoRunOn = autoRunOn;\n  factory _Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json);\n\n@override final  String name;\n@override final  String script;\n final  List<String>? _tags;\n@override List<String>? get tags {\n  final value = _tags;\n  if (value == null) return null;\n  if (_tags is EqualUnmodifiableListView) return _tags;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(value);\n}\n\n@override final  String? note;\n/// List of server id that this snippet should be auto run on\n final  List<String>? _autoRunOn;\n/// List of server id that this snippet should be auto run on\n@override List<String>? get autoRunOn {\n  final value = _autoRunOn;\n  if (value == null) return null;\n  if (_autoRunOn is EqualUnmodifiableListView) return _autoRunOn;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(value);\n}\n\n\n/// Create a copy of Snippet\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$SnippetCopyWith<_Snippet> get copyWith => __$SnippetCopyWithImpl<_Snippet>(this, _$identity);\n\n@override\nMap<String, dynamic> toJson() {\n  return _$SnippetToJson(this, );\n}\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _Snippet&&(identical(other.name, name) || other.name == name)&&(identical(other.script, script) || other.script == script)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.note, note) || other.note == note)&&const DeepCollectionEquality().equals(other._autoRunOn, _autoRunOn));\n}\n\n@JsonKey(includeFromJson: false, includeToJson: false)\n@override\nint get hashCode => Object.hash(runtimeType,name,script,const DeepCollectionEquality().hash(_tags),note,const DeepCollectionEquality().hash(_autoRunOn));\n\n@override\nString toString() {\n  return 'Snippet(name: $name, script: $script, tags: $tags, note: $note, autoRunOn: $autoRunOn)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$SnippetCopyWith<$Res> implements $SnippetCopyWith<$Res> {\n  factory _$SnippetCopyWith(_Snippet value, $Res Function(_Snippet) _then) = __$SnippetCopyWithImpl;\n@override @useResult\n$Res call({\n String name, String script, List<String>? tags, String? note, List<String>? autoRunOn\n});\n\n\n\n\n}\n/// @nodoc\nclass __$SnippetCopyWithImpl<$Res>\n    implements _$SnippetCopyWith<$Res> {\n  __$SnippetCopyWithImpl(this._self, this._then);\n\n  final _Snippet _self;\n  final $Res Function(_Snippet) _then;\n\n/// Create a copy of Snippet\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? script = null,Object? tags = freezed,Object? note = freezed,Object? autoRunOn = freezed,}) {\n  return _then(_Snippet(\nname: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable\nas String,script: null == script ? _self.script : script // ignore: cast_nullable_to_non_nullable\nas String,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable\nas List<String>?,note: freezed == note ? _self.note : note // ignore: cast_nullable_to_non_nullable\nas String?,autoRunOn: freezed == autoRunOn ? _self._autoRunOn : autoRunOn // ignore: cast_nullable_to_non_nullable\nas List<String>?,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/model/server/snippet.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'snippet.dart';\n\n// **************************************************************************\n// JsonSerializableGenerator\n// **************************************************************************\n\n_Snippet _$SnippetFromJson(Map<String, dynamic> json) => _Snippet(\n  name: json['name'] as String,\n  script: json['script'] as String,\n  tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),\n  note: json['note'] as String?,\n  autoRunOn: (json['autoRunOn'] as List<dynamic>?)\n      ?.map((e) => e as String)\n      .toList(),\n);\n\nMap<String, dynamic> _$SnippetToJson(_Snippet instance) => <String, dynamic>{\n  'name': instance.name,\n  'script': instance.script,\n  'tags': instance.tags,\n  'note': instance.note,\n  'autoRunOn': instance.autoRunOn,\n};\n"
  },
  {
    "path": "lib/data/model/server/system.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\n\nenum SystemType {\n  linux(linuxSign),\n  bsd(bsdSign),\n  windows(windowsSign);\n\n  final String? value;\n\n  const SystemType([this.value]);\n\n  static const linuxSign = '__linux';\n  static const bsdSign = '__bsd';\n  static const windowsSign = '__windows';\n\n  /// Used for parsing system types from shell output.\n  ///\n  /// This method looks for specific system signatures in the shell output\n  /// and returns the corresponding SystemType. If no signature is found,\n  /// it defaults to Linux but logs the detection failure for debugging.\n  static SystemType parse(String value) {\n    // Log the raw value for debugging purposes (truncated to avoid spam)\n    final truncatedValue = value.length > 100 ? '${value.substring(0, 100)}...' : value;\n\n    if (value.contains(windowsSign)) {\n      Loggers.app.info('System detected as Windows from signature in: $truncatedValue');\n      return SystemType.windows;\n    }\n    if (value.contains(bsdSign)) {\n      Loggers.app.info('System detected as BSD from signature in: $truncatedValue');\n      return SystemType.bsd;\n    }\n\n    // Log when falling back to Linux detection\n    if (value.trim().isEmpty) {\n      Loggers.app.warning(\n        'System detection received empty input, defaulting to Linux. '\n        'This may indicate a script execution issue.',\n      );\n    } else if (!value.contains(linuxSign)) {\n      Loggers.app.warning(\n        'System detection could not find any known signatures (Windows: $windowsSign, '\n        'BSD: $bsdSign, Linux: $linuxSign) in output: \"$truncatedValue\". '\n        'Defaulting to Linux, but this may cause incorrect parsing.',\n      );\n    } else {\n      Loggers.app.info('System detected as Linux from signature in: $truncatedValue');\n    }\n\n    return SystemType.linux;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/systemd.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\n\nenum SystemdUnitFunc {\n  start,\n  stop,\n  restart,\n  reload,\n  enable,\n  disable,\n  status;\n\n  IconData get icon => switch (this) {\n    start => Icons.play_arrow,\n    stop => Icons.stop,\n    restart => Icons.refresh,\n    reload => Icons.refresh,\n    enable => Icons.check,\n    disable => Icons.close,\n    status => Icons.info,\n  };\n}\n\nenum SystemdUnitType {\n  service,\n  socket,\n  mount,\n  timer;\n\n  static SystemdUnitType? fromString(String? value) {\n    return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());\n  }\n}\n\nenum SystemdUnitScope {\n  system,\n  user;\n\n  Color? get color => switch (this) {\n    system => Colors.red,\n    _ => null,\n  };\n\n  String getCmdPrefix(bool isRoot) {\n    if (this == system) {\n      return isRoot ? 'systemctl' : 'sudo systemctl';\n    }\n    return 'systemctl --user';\n  }\n}\n\nenum SystemdScopeFilter {\n  all,\n  system,\n  user;\n\n  String get displayName => switch (this) {\n    all => libL10n.all,\n    system => l10n.system,\n    user => libL10n.user,\n  };\n}\n\nenum SystemdUnitState {\n  active,\n  inactive,\n  failed,\n  activating,\n  deactivating;\n\n  static SystemdUnitState? fromString(String? value) {\n    return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());\n  }\n\n  Color? get color => switch (this) {\n    failed => Colors.red,\n    _ => null,\n  };\n}\n\nfinal class SystemdUnit {\n  final String name;\n  final String? description;\n  final SystemdUnitType type;\n  final SystemdUnitScope scope;\n  final SystemdUnitState state;\n\n  SystemdUnit({\n    required this.name,\n    this.description,\n    required this.type,\n    required this.scope,\n    required this.state,\n  });\n\n  String getCmd({required SystemdUnitFunc func, required bool isRoot}) {\n    final prefix = scope.getCmdPrefix(isRoot);\n    return '$prefix ${func.name} ${name.replaceAll(RegExp(r'[^a-zA-Z0-9\\-_.@:]'), '')}';\n  }\n\n  List<SystemdUnitFunc> get availableFuncs {\n    final funcs = <SystemdUnitFunc>{};\n    switch (state) {\n      case SystemdUnitState.active:\n        funcs.addAll([SystemdUnitFunc.stop, SystemdUnitFunc.restart]);\n        break;\n      case SystemdUnitState.inactive:\n        funcs.addAll([SystemdUnitFunc.start]);\n        break;\n      case SystemdUnitState.failed:\n        funcs.addAll([SystemdUnitFunc.restart]);\n        break;\n      case SystemdUnitState.activating:\n        funcs.addAll([SystemdUnitFunc.stop]);\n        break;\n      case SystemdUnitState.deactivating:\n        funcs.addAll([SystemdUnitFunc.start]);\n        break;\n    }\n    funcs.addAll([SystemdUnitFunc.status]);\n    return funcs.toList();\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/temp.dart",
    "content": "class Temperatures {\n  final Map<String, double> _map = {};\n\n  void parse(String type, String value) {\n    final typeSplited = type.split('\\n');\n    final valueSplited = value.split('\\n');\n    for (var i = 0; i < typeSplited.length && i < valueSplited.length; i++) {\n      final t = typeSplited[i];\n      final v = valueSplited[i];\n      if (t.isEmpty || v.isEmpty) {\n        continue;\n      }\n      final name = t.split('/').last;\n      final temp = double.tryParse(v);\n      if (temp == null) {\n        continue;\n      }\n      _map[name] = temp / 1000;\n    }\n  }\n\n  double? get(String name) {\n    return _map[name];\n  }\n\n  Iterable<String> get devices {\n    return _map.keys;\n  }\n\n  bool get isEmpty {\n    return _map.isEmpty;\n  }\n\n  double? get first {\n    if (_map.isEmpty) {\n      return null;\n    }\n    for (final key in _cpuTemp) {\n      if (_map.containsKey(key)) {\n        return _map[key];\n      }\n    }\n    return _map.values.firstOrNull;\n  }\n}\n\n/// soc: mobile phone\n/// cpu_thermal / x86_pkg_temp / coretemp / zenpower: x86\nconst _cpuTemp = ['x86_pkg_temp', 'coretemp', 'zenpower', 'cpu_thermal', 'soc'];\n"
  },
  {
    "path": "lib/data/model/server/time_seq.dart",
    "content": "import 'dart:collection';\n\nclass Fifo<T> extends ListBase<T> {\n  final int capacity;\n  late final List<T> _list;\n  var _count = 0;\n\n  Fifo({this.capacity = 30, List<T>? list}) : _list = list ?? <T>[];\n\n  @override\n  void add(element) {\n    _count++;\n    if (_list.length == capacity) {\n      _list.removeAt(0);\n    }\n    _list.add(element);\n  }\n\n  @override\n  int get length => _list.length;\n\n  int get count => _count;\n\n  @override\n  set length(int newLength) {\n    throw UnimplementedError();\n  }\n\n  @override\n  T operator [](int index) {\n    return _list[index];\n  }\n\n  @override\n  void operator []=(int index, value) {\n    _list[index] = value;\n  }\n}\n\nabstract class TimeSeq<T extends TimeSeqIface<T>> extends Fifo<List<T>> {\n  /// Due to the design, at least two elements are required, otherwise [pre] /\n  /// [now] will throw.\n  TimeSeq(List<T> init1, List<T> init2, {super.capacity}) : super(list: [init1, init2]);\n\n  List<T> get pre {\n    return _list[length - 2];\n  }\n\n  List<T> get now {\n    return _list[length - 1];\n  }\n\n  void onUpdate();\n\n  void update(List<T> new_) {\n    add(new_);\n\n    if (pre.length != now.length) {\n      final previous = pre.toList(growable: false);\n      final remaining = previous.toList(growable: true);\n      final aligned = <T>[];\n\n      for (final current in now) {\n        final matchIndex = remaining.indexWhere((item) => item.same(current));\n        if (matchIndex >= 0) {\n          aligned.add(remaining.removeAt(matchIndex));\n        } else {\n          aligned.add(current);\n        }\n      }\n\n      _list[length - 2] = aligned;\n    }\n\n    onUpdate();\n  }\n}\n\nabstract class TimeSeqIface<T> {\n  bool same(T other);\n}\n"
  },
  {
    "path": "lib/data/model/server/try_limiter.dart",
    "content": "import 'package:server_box/data/res/store.dart';\n\nclass TryLimiter {\n  final Map<String, int> _triedTimes = {};\n\n  static final _default = TryLimiter();\n\n  static bool canTry(String id) {\n    final maxCount = Stores.setting.maxRetryCount.fetch();\n    if (maxCount <= 0) {\n      return true;\n    }\n    final times = _default._triedTimes[id] ?? 0;\n    if (times >= maxCount) {\n      return false;\n    }\n    return true;\n  }\n\n  static void inc(String sid) {\n    _default._triedTimes[sid] = (_default._triedTimes[sid] ?? 0) + 1;\n  }\n\n  static void reset(String id) {\n    _default._triedTimes[id] = 0;\n  }\n\n  static void clear() {\n    _default._triedTimes.clear();\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/windows_parser.dart",
    "content": "import 'dart:convert';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:intl/intl.dart';\nimport 'package:server_box/data/model/server/cpu.dart';\nimport 'package:server_box/data/model/server/disk.dart';\nimport 'package:server_box/data/model/server/memory.dart';\nimport 'package:server_box/data/model/server/server.dart';\n\n/// Windows CPU parse result\nclass WindowsCpuResult {\n  final List<SingleCpuCore> cores;\n  final int coreCount;\n  const WindowsCpuResult(this.cores, this.coreCount);\n}\n\n/// Windows-specific status parsing utilities\n///\n/// This module handles parsing of Windows PowerShell command outputs\n/// for server monitoring. It extracts the Windows parsing logic\n/// to improve maintainability and readability.\nclass WindowsParser {\n  const WindowsParser._();\n\n  /// Parse Windows custom commands from parsed output\n  static void parseCustomCommands(\n    ServerStatus serverStatus,\n    Map<String, String> parsedOutput,\n    Map<String, String> customCmds,\n  ) {\n    try {\n      for (final entry in customCmds.entries) {\n        final key = entry.key;\n        final value = parsedOutput[key] ?? '';\n        serverStatus.customCmds[key] = value;\n      }\n    } catch (e, s) {\n      Loggers.app.warning('Windows custom commands parsing failed: $e', s);\n    }\n  }\n\n  /// Parse Windows uptime from PowerShell output\n  static String? parseUpTime(String raw) {\n    try {\n      // Clean the input - trim whitespace and get the first non-empty line\n      final cleanedInput = raw.trim().split('\\n').where((line) => line.trim().isNotEmpty).firstOrNull;\n\n      if (cleanedInput == null || cleanedInput.isEmpty) {\n        Loggers.app.warning('Windows uptime parsing: empty or null input');\n        return null;\n      }\n\n      // Try multiple date formats to handle different Windows locale/version outputs\n      final formatters = [\n        DateFormat('EEEE, MMMM d, yyyy h:mm:ss a', 'en_US'), // Original format\n        DateFormat('EEEE, MMMM dd, yyyy h:mm:ss a', 'en_US'), // Double-digit day\n        DateFormat('EEE, MMM d, yyyy h:mm:ss a', 'en_US'), // Shortened format\n        DateFormat('EEE, MMM dd, yyyy h:mm:ss a', 'en_US'), // Shortened with double-digit day\n        DateFormat('M/d/yyyy h:mm:ss a', 'en_US'), // Short US format\n        DateFormat('MM/dd/yyyy h:mm:ss a', 'en_US'), // Short US format with zero padding\n        DateFormat('d/M/yyyy h:mm:ss a', 'en_US'), // Short European format\n        DateFormat('dd/MM/yyyy h:mm:ss a', 'en_US'), // Short European format with zero padding\n      ];\n\n      DateTime? dateTime;\n      for (final formatter in formatters) {\n        dateTime = formatter.tryParseLoose(cleanedInput);\n        if (dateTime != null) break;\n      }\n\n      if (dateTime == null) {\n        Loggers.app.warning('Windows uptime parsing: could not parse date format for: $cleanedInput');\n        return null;\n      }\n\n      final now = DateTime.now();\n      final uptime = now.difference(dateTime);\n\n      // Validate that the uptime is reasonable (not negative, not too far in the future)\n      if (uptime.isNegative || uptime.inDays > 3650) {\n        // More than 10 years seems unreasonable\n        Loggers.app.warning(\n          'Windows uptime parsing: unreasonable uptime calculated: ${uptime.inDays} days for date: $cleanedInput',\n        );\n        return null;\n      }\n\n      final days = uptime.inDays;\n      final hours = uptime.inHours % 24;\n      final minutes = uptime.inMinutes % 60;\n\n      if (days > 0) {\n        return '$days days, $hours:${minutes.toString().padLeft(2, '0')}';\n      } else {\n        return '$hours:${minutes.toString().padLeft(2, '0')}';\n      }\n    } catch (e, s) {\n      Loggers.app.warning('Windows uptime parsing failed: $e for input: $raw', s);\n      return null;\n    }\n  }\n\n  /// Parse Windows CPU information from PowerShell output\n  /// Returns WindowsCpuResult containing CPU cores and total core count\n  static WindowsCpuResult parseCpu(String raw, ServerStatus serverStatus) {\n    try {\n      final dynamic jsonData = json.decode(raw);\n      final List<SingleCpuCore> cpus = [];\n      int totalCoreCount = 1;\n\n      if (jsonData is List) {\n        // Multiple physical processors\n        totalCoreCount = 0; // Reset to sum up\n        var logicalProcessorOffset = 0;\n        final prevCpus = serverStatus.cpu.now;\n        for (int procIdx = 0; procIdx < jsonData.length; procIdx++) {\n          final processor = jsonData[procIdx];\n          final loadPercentage = (processor['LoadPercentage'] as num?) ?? 0;\n          final numberOfCores = (processor['NumberOfCores'] as int?) ?? 1;\n          final numberOfLogicalProcessors = (processor['NumberOfLogicalProcessors'] as int?) ?? numberOfCores;\n          totalCoreCount += numberOfCores;\n          final usage = loadPercentage.toInt();\n          final idle = 100 - usage;\n\n          // Create a SingleCpuCore entry for each logical processor\n          // Windows only reports overall CPU load, so we distribute it evenly\n          for (int i = 0; i < numberOfLogicalProcessors; i++) {\n            final coreId = logicalProcessorOffset + i;\n            // Skip summary entry at index 0 when looking up previous samples\n            final prevIndex = coreId + 1;\n            final prevCpu = prevIndex < prevCpus.length ? prevCpus[prevIndex] : null;\n\n            // LIMITATION: Windows CPU counters approach\n            // PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.\n            // We simulate cumulative counters by adding current percentages to previous totals.\n            // Additionally, Windows only provides overall CPU load, not per-core load.\n            // We distribute the load evenly across all logical processors.\n            final newUser = (prevCpu?.user ?? 0) + usage;\n            final newIdle = (prevCpu?.idle ?? 0) + idle;\n\n            cpus.add(\n              SingleCpuCore(\n                'cpu$coreId',\n                newUser, // cumulative user time\n                0, // sys (not available)\n                0, // nice (not available)\n                newIdle, // cumulative idle time\n                0, // iowait (not available)\n                0, // irq (not available)\n                0, // softirq (not available)\n              ),\n            );\n          }\n          logicalProcessorOffset += numberOfLogicalProcessors;\n        }\n      } else if (jsonData is Map) {\n        // Single physical processor\n        final loadPercentage = (jsonData['LoadPercentage'] as num?) ?? 0;\n        final numberOfCores = (jsonData['NumberOfCores'] as int?) ?? 1;\n        final numberOfLogicalProcessors = (jsonData['NumberOfLogicalProcessors'] as int?) ?? numberOfCores;\n        totalCoreCount = numberOfCores;\n        final usage = loadPercentage.toInt();\n        final idle = 100 - usage;\n\n        // Create a SingleCpuCore entry for each logical processor\n        final prevCpus = serverStatus.cpu.now;\n        for (int i = 0; i < numberOfLogicalProcessors; i++) {\n          // Skip summary entry at index 0 when looking up previous samples\n          final prevIndex = i + 1;\n          final prevCpu = prevIndex < prevCpus.length ? prevCpus[prevIndex] : null;\n\n          // LIMITATION: See comment above for Windows CPU counter limitations\n          final newUser = (prevCpu?.user ?? 0) + usage;\n          final newIdle = (prevCpu?.idle ?? 0) + idle;\n\n          cpus.add(\n            SingleCpuCore(\n              'cpu$i',\n              newUser, // cumulative user time\n              0, // sys\n              0, // nice\n              newIdle, // cumulative idle time\n              0, // iowait\n              0, // irq\n              0, // softirq\n            ),\n          );\n        }\n      }\n\n      // Add a summary entry at the beginning (like Linux 'cpu' line)\n      // This is the aggregate of all logical processors\n      if (cpus.isNotEmpty) {\n        int totalUser = 0;\n        int totalIdle = 0;\n        for (final core in cpus) {\n          totalUser += core.user;\n          totalIdle += core.idle;\n        }\n        // Insert at the beginning with ID 'cpu' (matching Linux format)\n        cpus.insert(0, SingleCpuCore(\n          'cpu', // Summary entry, like Linux\n          totalUser,\n          0,\n          0,\n          totalIdle,\n          0,\n          0,\n          0,\n        ));\n      }\n\n      return WindowsCpuResult(cpus, totalCoreCount);\n    } catch (e, s) {\n      Loggers.app.warning('Windows CPU parsing failed: $e', s);\n      return WindowsCpuResult([], 1);\n    }\n  }\n\n  /// Parse Windows memory information from PowerShell output\n  ///\n  /// NOTE: Windows Win32_OperatingSystem properties TotalVisibleMemorySize\n  /// and FreePhysicalMemory are returned in KB units.\n  static Memory? parseMemory(String raw) {\n    try {\n      final dynamic jsonData = json.decode(raw);\n      final data = jsonData is List ? jsonData.first : jsonData;\n\n      // Win32_OperatingSystem properties are in KB\n      final totalKB = data['TotalVisibleMemorySize'] as int? ?? 0;\n      final freeKB = data['FreePhysicalMemory'] as int? ?? 0;\n\n      return Memory(\n        total: totalKB,\n        free: freeKB,\n        avail: freeKB, // Windows doesn't distinguish between free and available\n      );\n    } catch (e) {\n      return null;\n    }\n  }\n\n  /// Parse Windows disk information from PowerShell output\n  static List<Disk> parseDisks(String raw) {\n    try {\n      final dynamic jsonData = json.decode(raw);\n      final List<Disk> disks = [];\n\n      final diskList = jsonData is List ? jsonData : [jsonData];\n\n      for (final diskData in diskList) {\n        final deviceId = diskData['DeviceID']?.toString() ?? '';\n        final size = BigInt.tryParse(diskData['Size']?.toString() ?? '0') ?? BigInt.zero;\n        final freeSpace = BigInt.tryParse(diskData['FreeSpace']?.toString() ?? '0') ?? BigInt.zero;\n        final fileSystem = diskData['FileSystem']?.toString() ?? '';\n\n        // Validate all required fields\n        final hasRequiredFields =\n            deviceId.isNotEmpty && size != BigInt.zero && freeSpace != BigInt.zero && fileSystem.isNotEmpty;\n\n        if (!hasRequiredFields) {\n          Loggers.app.warning(\n            'Windows disk parsing: skipping disk with missing required fields. '\n            'DeviceID: $deviceId, Size: $size, FreeSpace: $freeSpace, FileSystem: $fileSystem',\n          );\n          continue;\n        }\n\n        final sizeKB = size ~/ BigInt.from(1024);\n        final freeKB = freeSpace ~/ BigInt.from(1024);\n        final usedKB = sizeKB - freeKB;\n        final usedPercent = sizeKB > BigInt.zero\n            ? ((usedKB * BigInt.from(100)) ~/ sizeKB).toInt().clamp(0, 100)\n            : 0;\n\n        disks.add(\n          Disk(\n            path: deviceId,\n            fsTyp: fileSystem,\n            size: sizeKB,\n            avail: freeKB,\n            used: usedKB,\n            usedPercent: usedPercent,\n            mount: deviceId, // Windows uses drive letters as mount points\n          ),\n        );\n      }\n\n      return disks;\n    } catch (e) {\n      Loggers.app.warning('Windows disk parsing failed: $e');\n      return [];\n    }\n  }\n}\n"
  },
  {
    "path": "lib/data/model/server/wol_cfg.dart",
    "content": "import 'dart:io';\n\nimport 'package:json_annotation/json_annotation.dart';\nimport 'package:wake_on_lan/wake_on_lan.dart';\n\npart 'wol_cfg.g.dart';\n\n@JsonSerializable(includeIfNull: false)\nfinal class WakeOnLanCfg {\n  final String mac;\n  final String ip;\n  final String? pwd;\n\n  const WakeOnLanCfg({required this.mac, required this.ip, this.pwd});\n\n  (Object?, bool) validate() {\n    final macValidation = MACAddress.validate(mac);\n    final ipValidation = IPAddress.validate(\n      ip,\n      type: ip.contains(':') ? InternetAddressType.IPv6 : InternetAddressType.IPv4,\n    );\n    final pwdValidation = pwd != null ? SecureONPassword.validate(pwd) : (state: true, error: null);\n\n    final valid = macValidation.state && ipValidation.state && pwdValidation.state;\n    final err = macValidation.error ?? ipValidation.error ?? pwdValidation.error;\n    return (err, valid);\n  }\n\n  Future<void> wake() {\n    if (!validate().$2) {\n      throw Exception('Invalid WakeOnLanCfg');\n    }\n\n    final ip_ = IPAddress(ip);\n    final mac_ = MACAddress(mac);\n    final pwd_ = pwd != null ? SecureONPassword(pwd!) : null;\n    final obj = WakeOnLAN(ip_, mac_, password: pwd_);\n    return obj.wake(repeat: 3, repeatDelay: const Duration(milliseconds: 500));\n  }\n\n  factory WakeOnLanCfg.fromJson(Map<String, dynamic> json) => _$WakeOnLanCfgFromJson(json);\n\n  Map<String, dynamic> toJson() => _$WakeOnLanCfgToJson(this);\n}\n"
  },
  {
    "path": "lib/data/model/server/wol_cfg.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'wol_cfg.dart';\n\n// **************************************************************************\n// JsonSerializableGenerator\n// **************************************************************************\n\nWakeOnLanCfg _$WakeOnLanCfgFromJson(Map<String, dynamic> json) => WakeOnLanCfg(\n  mac: json['mac'] as String,\n  ip: json['ip'] as String,\n  pwd: json['pwd'] as String?,\n);\n\nMap<String, dynamic> _$WakeOnLanCfgToJson(WakeOnLanCfg instance) =>\n    <String, dynamic>{\n      'mac': instance.mac,\n      'ip': instance.ip,\n      'pwd': ?instance.pwd,\n    };\n"
  },
  {
    "path": "lib/data/model/sftp/browser_status.dart",
    "content": "import 'package:dartssh2/dartssh2.dart';\nimport 'package:fl_lib/fl_lib.dart';\n\n/// Remote server only can be linux-like system, so use '/' as seperator\nconst _sep = '/';\n\nclass SftpBrowserStatus {\n  final List<SftpName> files = [];\n  final path = _AbsolutePath(_sep);\n  SftpClient? client;\n\n  SftpBrowserStatus(SSHClient client) {\n    client.sftp().then((value) => this.client = value);\n  }\n}\n\nclass _AbsolutePath {\n  String _path;\n  final _prePath = <String>[];\n\n  _AbsolutePath(this._path);\n\n  String get path => _path;\n\n  /// Update path, not set path\n  set path(String newPath) {\n    _prePath.add(_path);\n    if (newPath == '..') {\n      _path = _path.substring(0, _path.lastIndexOf(_sep));\n      if (_path == '') {\n        _path = _sep;\n      }\n      return;\n    }\n    if (newPath.startsWith(_sep)) {\n      _path = newPath;\n      return;\n    }\n    _path = _path.joinPath(newPath, separator: _sep);\n  }\n\n  bool undo() {\n    if (_prePath.isEmpty) {\n      return false;\n    }\n    _path = _prePath.removeLast();\n    return true;\n  }\n}\n"
  },
  {
    "path": "lib/data/model/sftp/req.dart",
    "content": "part of 'worker.dart';\n\nclass SftpReq {\n  final Spi spi;\n  final String remotePath;\n  final String localPath;\n  final SftpReqType type;\n  String? privateKey;\n  Spi? jumpSpi;\n  String? jumpPrivateKey;\n  Map<String, Spi>? jumpSpisById;\n  Map<String, String>? privateKeysByKeyId;\n  Map<String, String>? knownHostFingerprints;\n\n  SftpReq(this.spi, this.remotePath, this.localPath, this.type) {\n    privateKeysByKeyId = {};\n\n    final keyId = spi.keyId;\n    if (keyId != null) {\n      privateKey = getPrivateKey(keyId);\n      privateKeysByKeyId![keyId] = privateKey!;\n    }\n\n    final allServers = {\n      for (final server in Stores.server.fetch()) server.id: server,\n    };\n    jumpSpisById = collectJumpServers(spi: spi, serversById: allServers);\n\n    if (spi.jumpId != null) {\n      jumpSpi = jumpSpisById?[spi.jumpId];\n      jumpPrivateKey = Stores.key.fetchOne(jumpSpi?.keyId)?.key;\n      if (jumpSpi?.keyId case final jumpKeyId?) {\n        if (jumpPrivateKey != null) {\n          privateKeysByKeyId![jumpKeyId] = jumpPrivateKey!;\n        }\n      }\n    }\n\n    for (final jump in jumpSpisById?.values ?? const <Spi>[]) {\n      final jumpKeyId = jump.keyId;\n      if (jumpKeyId == null || privateKeysByKeyId!.containsKey(jumpKeyId)) {\n        continue;\n      }\n      final key = Stores.key.fetchOne(jumpKeyId)?.key;\n      if (key == null) {\n        continue;\n      }\n      privateKeysByKeyId![jumpKeyId] = key;\n    }\n\n    if (jumpSpisById != null && jumpSpisById!.isEmpty) {\n      jumpSpisById = null;\n    }\n    if (privateKeysByKeyId != null && privateKeysByKeyId!.isEmpty) {\n      privateKeysByKeyId = null;\n    }\n\n    try {\n      knownHostFingerprints = Map<String, String>.from(\n        Stores.setting.sshKnownHostFingerprints.get(),\n      );\n    } catch (e, s) {\n      Loggers.app.warning('Failed to load SSH known host fingerprints', e, s);\n      knownHostFingerprints = null;\n    }\n  }\n}\n\nenum SftpReqType { download, upload }\n\nclass SftpReqStatus {\n  final int id;\n  final SftpReq req;\n  final void Function() notifyListeners;\n  late SftpWorker worker;\n  final Completer? completer;\n\n  String get fileName => req.localPath.split(Pfs.seperator).last;\n\n  // status of the download\n  double? progress;\n  SftpWorkerStatus? status;\n  int? size;\n  Exception? error;\n  Duration? spentTime;\n\n  SftpReqStatus({\n    required this.req,\n    required this.notifyListeners,\n    this.completer,\n  }) : id = DateTime.now().microsecondsSinceEpoch {\n    worker = SftpWorker(onNotify: onNotify, req: req)..init();\n  }\n\n  @override\n  bool operator ==(Object other) => other is SftpReqStatus && id == other.id;\n\n  @override\n  int get hashCode => id ^ super.hashCode;\n\n  void dispose() {\n    worker._dispose();\n    completer?.complete(true);\n  }\n\n  void onNotify(dynamic event) {\n    var shouldDispose = false;\n    switch (event) {\n      case final SftpWorkerStatus val:\n        status = val;\n        if (status == SftpWorkerStatus.finished) {\n          dispose();\n        }\n        break;\n      case final double val:\n        progress = val;\n        break;\n      case final int val:\n        size = val;\n        break;\n      case final Duration d:\n        spentTime = d;\n        break;\n      default:\n        error = Exception('sftp worker event: $event');\n        Loggers.app.warning(error);\n        shouldDispose = true;\n    }\n    notifyListeners();\n    if (shouldDispose) dispose();\n  }\n}\n\nenum SftpWorkerStatus { preparing, sshConnectted, loading, finished }\n"
  },
  {
    "path": "lib/data/model/sftp/worker.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\nimport 'dart:isolate';\nimport 'dart:typed_data';\n\nimport 'package:dartssh2/dartssh2.dart';\nimport 'package:easy_isolate/easy_isolate.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/core/utils/jump_chain.dart';\nimport 'package:server_box/core/utils/server.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/res/store.dart';\n\npart 'req.dart';\n\nclass SftpWorker {\n  final Function(Object event) onNotify;\n  final SftpReq req;\n\n  final worker = Worker();\n\n  SftpWorker({required this.onNotify, required this.req});\n\n  void _dispose() {\n    worker.dispose();\n  }\n\n  /// Initiate the worker (new thread) and start listen from messages between\n  /// the threads\n  Future<void> init() async {\n    if (worker.isInitialized) worker.dispose();\n    await worker.init(\n      mainMessageHandler,\n      isolateMessageHandler,\n      errorHandler: print,\n    );\n    worker.sendMessage(req);\n  }\n\n  /// Handle the messages coming from the isolate\n  void mainMessageHandler(dynamic data, SendPort isolateSendPort) {\n    onNotify(data);\n  }\n}\n\n/// Handle the messages coming from the main\nFuture<void> isolateMessageHandler(\n  dynamic data,\n  SendPort mainSendPort,\n  SendErrorFunction sendError,\n) async {\n  switch (data) {\n    case final SftpReq val:\n      switch (val.type) {\n        case SftpReqType.download:\n          await _download(data, mainSendPort, sendError);\n          break;\n        case SftpReqType.upload:\n          await _upload(data, mainSendPort, sendError);\n          break;\n      }\n      break;\n    default:\n      sendError(Exception('unknown event'));\n  }\n}\n\nFuture<void> _download(\n  SftpReq req,\n  SendPort mainSendPort,\n  SendErrorFunction sendError,\n) async {\n  try {\n    mainSendPort.send(SftpWorkerStatus.preparing);\n    final watch = Stopwatch()..start();\n    final client = await genClient(\n      req.spi,\n      privateKey: req.privateKey,\n      jumpSpi: req.jumpSpi,\n      jumpPrivateKey: req.jumpPrivateKey,\n      privateKeysByKeyId: req.privateKeysByKeyId,\n      jumpSpisById: req.jumpSpisById,\n      knownHostFingerprints: req.knownHostFingerprints,\n    );\n    mainSendPort.send(SftpWorkerStatus.sshConnectted);\n\n    /// Create the directory if not exists\n    final dirPath = req.localPath.substring(\n      0,\n      req.localPath.lastIndexOf(Pfs.seperator),\n    );\n    await Directory(dirPath).create(recursive: true);\n\n    /// Use [FileMode.write] to overwrite the file\n    final localFile = File(req.localPath).openWrite(mode: FileMode.write);\n    final file = await (await client.sftp()).open(req.remotePath);\n    final size = (await file.stat()).size;\n    if (size == null) {\n      mainSendPort.send(Exception('can\\'t get file size: ${req.remotePath}'));\n      return;\n    }\n\n    mainSendPort.send(size);\n    mainSendPort.send(SftpWorkerStatus.loading);\n\n    // Issue #161\n    // Due to single core performance, limit the chunk size\n    const defaultChunkSize = 1024 * 1024 * 5;\n    var totalRead = 0;\n\n    while (totalRead < size) {\n      final remaining = size - totalRead;\n      final chunkSize = remaining > defaultChunkSize\n          ? defaultChunkSize\n          : remaining;\n      dprint('Size: $size, Total Read: $totalRead, Chunk Size: $chunkSize');\n\n      final fileData = file.read(offset: totalRead, length: chunkSize);\n      await for (var chunk in fileData) {\n        localFile.add(chunk);\n        totalRead += chunk.length;\n        mainSendPort.send(totalRead / size * 100);\n      }\n    }\n\n    await localFile.close();\n    await file.close();\n\n    mainSendPort.send(watch.elapsed);\n    mainSendPort.send(SftpWorkerStatus.finished);\n  } catch (e) {\n    mainSendPort.send(e);\n  }\n}\n\nFuture<void> _upload(\n  SftpReq req,\n  SendPort mainSendPort,\n  SendErrorFunction sendError,\n) async {\n  try {\n    mainSendPort.send(SftpWorkerStatus.preparing);\n    final watch = Stopwatch()..start();\n    final client = await genClient(\n      req.spi,\n      privateKey: req.privateKey,\n      jumpSpi: req.jumpSpi,\n      jumpPrivateKey: req.jumpPrivateKey,\n      privateKeysByKeyId: req.privateKeysByKeyId,\n      jumpSpisById: req.jumpSpisById,\n      knownHostFingerprints: req.knownHostFingerprints,\n    );\n    mainSendPort.send(SftpWorkerStatus.sshConnectted);\n\n    final local = File(req.localPath);\n    if (!await local.exists()) {\n      mainSendPort.send(Exception('local file not exists'));\n      return;\n    }\n    final localLen = await local.length();\n    mainSendPort.send(localLen);\n    mainSendPort.send(SftpWorkerStatus.loading);\n    final localFile = local.openRead().cast<Uint8List>();\n    final sftp = await client.sftp();\n    // If remote exists, overwrite it\n    final file = await sftp.open(\n      req.remotePath,\n      mode:\n          SftpFileOpenMode.truncate |\n          SftpFileOpenMode.create |\n          SftpFileOpenMode.write,\n    );\n    final writer = file.write(\n      localFile,\n      onProgress: (total) {\n        mainSendPort.send(total / localLen * 100);\n      },\n    );\n    await writer.done;\n    await file.close();\n    mainSendPort.send(watch.elapsed);\n    mainSendPort.send(SftpWorkerStatus.finished);\n  } catch (e) {\n    mainSendPort.send(e);\n  }\n}\n"
  },
  {
    "path": "lib/data/model/ssh/virtual_key.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:xterm/core.dart';\n\nenum VirtualKeyFunc { toggleIME, backspace, clipboard, snippet, file }\n\nenum VirtKey {\n  esc,\n  alt,\n  home,\n  up,\n  end,\n  sftp,\n  snippet,\n  tab,\n  ctrl,\n  left,\n  down,\n  right,\n  clipboard,\n  ime,\n  shift,\n  pgup,\n  pgdn,\n  slash,\n  backSlash,\n  underscore,\n  plus,\n  equal,\n  minus,\n  parenLeft,\n  parenRight,\n  bracketLeft,\n  bracketRight,\n  braceLeft,\n  braceRight,\n  chevronLeft,\n  chevronRight,\n  colon,\n  semicolon,\n  f1,\n  f2,\n  f3,\n  f4,\n  f5,\n  f6,\n  f7,\n  f8,\n  f9,\n  f10,\n  f11,\n  f12,\n}\n\nextension VirtKeyX on VirtKey {\n  /// Used for input to terminal\n  String? get inputRaw => switch (this) {\n    VirtKey.slash => '/',\n    VirtKey.backSlash => '\\\\',\n    VirtKey.underscore => '_',\n    VirtKey.plus => '+',\n    VirtKey.equal => '=',\n    VirtKey.minus => '-',\n    VirtKey.parenLeft => '(',\n    VirtKey.parenRight => ')',\n    VirtKey.bracketLeft => '[',\n    VirtKey.bracketRight => ']',\n    VirtKey.braceLeft => '{',\n    VirtKey.braceRight => '}',\n    VirtKey.chevronLeft => '<',\n    VirtKey.chevronRight => '>',\n    VirtKey.colon => ':',\n    VirtKey.semicolon => ';',\n    _ => null,\n  };\n\n  /// Used for displaying on UI\n  String get text {\n    final t = inputRaw;\n    if (t != null) return t;\n\n    if (this == VirtKey.pgdn) return 'PgDn';\n    if (this == VirtKey.pgup) return 'PgUp';\n\n    if (name.length > 1) {\n      return name.substring(0, 1).toUpperCase() + name.substring(1);\n    }\n    return name;\n  }\n\n  /// Default order of virtual keys\n  static const defaultOrder = [\n    VirtKey.esc,\n    VirtKey.alt,\n    VirtKey.home,\n    VirtKey.up,\n    VirtKey.end,\n    VirtKey.sftp,\n    VirtKey.snippet,\n    VirtKey.tab,\n    VirtKey.ctrl,\n    VirtKey.left,\n    VirtKey.down,\n    VirtKey.right,\n    VirtKey.clipboard,\n    VirtKey.ime,\n    VirtKey.shift,\n  ];\n\n  /// Corresponding [TerminalKey]\n  TerminalKey? get key => switch (this) {\n    VirtKey.esc => TerminalKey.escape,\n    VirtKey.alt => TerminalKey.alt,\n    VirtKey.home => TerminalKey.home,\n    VirtKey.up => TerminalKey.arrowUp,\n    VirtKey.end => TerminalKey.end,\n    VirtKey.tab => TerminalKey.tab,\n    VirtKey.ctrl => TerminalKey.control,\n    VirtKey.left => TerminalKey.arrowLeft,\n    VirtKey.down => TerminalKey.arrowDown,\n    VirtKey.right => TerminalKey.arrowRight,\n    VirtKey.shift => TerminalKey.shift,\n    VirtKey.pgup => TerminalKey.pageUp,\n    VirtKey.pgdn => TerminalKey.pageDown,\n    VirtKey.f1 => TerminalKey.f1,\n    VirtKey.f2 => TerminalKey.f2,\n    VirtKey.f3 => TerminalKey.f3,\n    VirtKey.f4 => TerminalKey.f4,\n    VirtKey.f5 => TerminalKey.f5,\n    VirtKey.f6 => TerminalKey.f6,\n    VirtKey.f7 => TerminalKey.f7,\n    VirtKey.f8 => TerminalKey.f8,\n    VirtKey.f9 => TerminalKey.f9,\n    VirtKey.f10 => TerminalKey.f10,\n    VirtKey.f11 => TerminalKey.f11,\n    VirtKey.f12 => TerminalKey.f12,\n    _ => null,\n  };\n\n  /// Icons for virtual keys\n  IconData? get icon => switch (this) {\n    VirtKey.up => Icons.arrow_upward,\n    VirtKey.left => Icons.arrow_back,\n    VirtKey.down => Icons.arrow_downward,\n    VirtKey.right => Icons.arrow_forward,\n    VirtKey.sftp => Icons.file_open,\n    VirtKey.snippet => Icons.code,\n    VirtKey.clipboard => Icons.paste,\n    VirtKey.ime => Icons.keyboard,\n    _ => null,\n  };\n\n  // Use [VirtualKeyFunc] instead of [VirtKey]\n  // This can help linter to enum all [VirtualKeyFunc]\n  // and make sure all [VirtualKeyFunc] are handled\n  VirtualKeyFunc? get func => switch (this) {\n    VirtKey.sftp => VirtualKeyFunc.file,\n    VirtKey.snippet => VirtualKeyFunc.snippet,\n    VirtKey.clipboard => VirtualKeyFunc.clipboard,\n    VirtKey.ime => VirtualKeyFunc.toggleIME,\n    _ => null,\n  };\n\n  bool get toggleable => switch (this) {\n    VirtKey.alt || VirtKey.ctrl || VirtKey.shift => true,\n    _ => false,\n  };\n\n  bool get canLongPress => switch (this) {\n    VirtKey.up || VirtKey.left || VirtKey.down || VirtKey.right => true,\n    _ => false,\n  };\n\n  String? get help => switch (this) {\n    VirtKey.sftp => l10n.virtKeyHelpSFTP,\n    VirtKey.clipboard => l10n.virtKeyHelpClipboard,\n    VirtKey.ime => l10n.virtKeyHelpIME,\n    _ => null,\n  };\n\n  /// - [saveDefaultIfErr] if the stored raw values is invalid, save default order to store\n  static List<VirtKey> loadFromStore({bool saveDefaultIfErr = true}) {\n    try {\n      final ints = Stores.setting.sshVirtKeys.fetch();\n      return ints.map((e) => VirtKey.values[e]).toList();\n    } on RangeError {\n      final ints = defaultOrder.map((e) => e.index).toList();\n      Stores.setting.sshVirtKeys.put(ints);\n    } catch (e, s) {\n      Loggers.app.warning('Failed to load sshVirtKeys', e, s);\n    }\n    return defaultOrder;\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/ai/ask_ai.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:dio/dio.dart';\nimport 'package:meta/meta.dart';\nimport 'package:riverpod/riverpod.dart';\nimport 'package:server_box/data/model/ai/ask_ai_models.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/data/store/setting.dart';\n\nfinal askAiRepositoryProvider = Provider<AskAiRepository>((ref) {\n  return AskAiRepository();\n});\n\nclass AskAiRepository {\n  AskAiRepository({Dio? dio}) : _dio = dio ?? Dio();\n\n  final Dio _dio;\n\n  SettingStore get _settings => Stores.setting;\n\n  /// Streams the AI response using the configured endpoint.\n  Stream<AskAiEvent> ask({\n    required String selection,\n    String? localeHint,\n    List<AskAiMessage> conversation = const [],\n  }) async* {\n    final baseUrl = _settings.askAiBaseUrl.fetch().trim();\n    final apiKey = _settings.askAiApiKey.fetch().trim();\n    final model = _settings.askAiModel.fetch().trim();\n\n    final missing = <AskAiConfigField>[];\n    if (baseUrl.isEmpty) missing.add(AskAiConfigField.baseUrl);\n    if (apiKey.isEmpty) missing.add(AskAiConfigField.apiKey);\n    if (model.isEmpty) missing.add(AskAiConfigField.model);\n    if (missing.isNotEmpty) {\n      throw AskAiConfigException(missingFields: missing);\n    }\n\n    final parsedBaseUri = Uri.tryParse(baseUrl);\n    final hasScheme = parsedBaseUri?.hasScheme ?? false;\n    final hasHost = (parsedBaseUri?.host ?? '').isNotEmpty;\n    if (!hasScheme || !hasHost) {\n      throw AskAiConfigException(invalidBaseUrl: baseUrl);\n    }\n\n    final uri = _composeUri(baseUrl, '/v1/chat/completions');\n    final authHeader = apiKey.startsWith('Bearer ') ? apiKey : 'Bearer $apiKey';\n    final headers = <String, String>{\n      Headers.acceptHeader: 'text/event-stream',\n      Headers.contentTypeHeader: Headers.jsonContentType,\n      'Authorization': authHeader,\n    };\n\n    final requestBody = _buildRequestBody(\n      model: model,\n      selection: selection,\n      localeHint: localeHint,\n      conversation: conversation,\n    );\n\n    Response<ResponseBody> response;\n    try {\n      response = await _dio.postUri<ResponseBody>(\n        uri,\n        data: jsonEncode(requestBody),\n        options: Options(\n          responseType: ResponseType.stream,\n          headers: headers,\n          sendTimeout: const Duration(seconds: 20),\n          receiveTimeout: const Duration(minutes: 2),\n        ),\n      );\n    } on DioException catch (e) {\n      throw AskAiNetworkException(message: e.message ?? 'Request failed', cause: e);\n    }\n\n    final body = response.data;\n    if (body == null) {\n      throw AskAiNetworkException(message: 'Empty response body');\n    }\n\n    final contentBuffer = StringBuffer();\n    final commands = <AskAiCommand>[];\n    final toolBuilders = <int, _ToolCallBuilder>{};\n    final utf8Stream = body.stream.cast<List<int>>().transform(utf8.decoder);\n    final carry = StringBuffer();\n\n    try {\n      await for (final chunk in utf8Stream) {\n        carry.write(chunk);\n        final segments = carry.toString().split('\\n\\n');\n        carry\n          ..clear()\n          ..write(segments.removeLast());\n\n        for (final segment in segments) {\n          final lines = segment.split('\\n');\n          for (final rawLine in lines) {\n            final line = rawLine.trim();\n            if (line.isEmpty || !line.startsWith('data:')) {\n              continue;\n            }\n            final payload = line.substring(5).trim();\n            if (payload.isEmpty) {\n              continue;\n            }\n            if (payload == '[DONE]') {\n              yield AskAiCompleted(\n                fullText: contentBuffer.toString(),\n                commands: List.unmodifiable(commands),\n              );\n              return;\n            }\n\n            Map<String, dynamic> json;\n            try {\n              json = jsonDecode(payload) as Map<String, dynamic>;\n            } catch (e, s) {\n              yield AskAiStreamError(e, s);\n              continue;\n            }\n\n            final choices = json['choices'];\n            if (choices is! List || choices.isEmpty) {\n              continue;\n            }\n\n            for (final choice in choices) {\n              if (choice is! Map<String, dynamic>) {\n                continue;\n              }\n              final delta = choice['delta'];\n              if (delta is Map<String, dynamic>) {\n                final content = delta['content'];\n                if (content is String && content.isNotEmpty) {\n                  contentBuffer.write(content);\n                  yield AskAiContentDelta(content);\n                } else if (content is List) {\n                  for (final item in content) {\n                    if (item is Map<String, dynamic>) {\n                      final text = item['text'] as String?;\n                      if (text != null && text.isNotEmpty) {\n                        contentBuffer.write(text);\n                        yield AskAiContentDelta(text);\n                      }\n                    }\n                  }\n                }\n\n                final toolCalls = delta['tool_calls'];\n                if (toolCalls is List) {\n                  for (final toolCall in toolCalls) {\n                    if (toolCall is! Map<String, dynamic>) continue;\n                    final index = toolCall['index'] as int? ?? 0;\n                    final builder = toolBuilders.putIfAbsent(index, _ToolCallBuilder.new);\n                    final function = toolCall['function'];\n                    if (function is Map<String, dynamic>) {\n                      builder.name ??= function['name'] as String?;\n                      final args = function['arguments'] as String?;\n                      if (args != null && args.isNotEmpty) {\n                        builder.arguments.write(args);\n                        final command = builder.tryBuild();\n                        if (command != null) {\n                          commands.add(command);\n                          yield AskAiToolSuggestion(command);\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n\n              final finishReason = choice['finish_reason'];\n              if (finishReason == 'tool_calls') {\n                for (final builder in toolBuilders.values) {\n                  final command = builder.tryBuild(force: true);\n                  if (command != null) {\n                    commands.add(command);\n                    yield AskAiToolSuggestion(command);\n                  }\n                }\n                toolBuilders.clear();\n              }\n            }\n          }\n        }\n      }\n\n      // Flush remaining buffer if [DONE] not received.\n      if (contentBuffer.isNotEmpty || commands.isNotEmpty) {\n        yield AskAiCompleted(\n          fullText: contentBuffer.toString(),\n          commands: List.unmodifiable(commands),\n        );\n      }\n    } catch (e, s) {\n      yield AskAiStreamError(e, s);\n      return;\n    }\n  }\n\n  Map<String, dynamic> _buildRequestBody({\n    required String model,\n    required String selection,\n    required List<AskAiMessage> conversation,\n    String? localeHint,\n  }) {\n    final promptBuffer = StringBuffer()\n      ..writeln('你是一个 SSH 终端助手。')\n      ..writeln('用户会提供一段终端输出或命令，请结合上下文给出解释。')\n      ..writeln('当需要给出可执行命令时，调用 `recommend_shell` 工具，并提供简短描述。')\n      ..writeln('仅在非常确定命令安全时才给出建议。');\n\n    if (localeHint != null && localeHint.isNotEmpty) {\n      promptBuffer\n        .writeln('请优先使用用户的语言输出：$localeHint。');\n    }\n\n    final messages = <Map<String, String>>[\n      {\n        'role': 'system',\n        'content': promptBuffer.toString(),\n      },\n      ...conversation.map((message) => {\n            'role': message.apiRole,\n            'content': message.content,\n          }),\n      {\n        'role': 'user',\n        'content': '以下是终端选中的内容：\\n$selection',\n      },\n    ];\n\n    return {\n      'model': model,\n      'stream': true,\n      'messages': messages,\n      'tools': [\n        {\n          'type': 'function',\n          'function': {\n            'name': 'recommend_shell',\n            'description': '返回一个用户可以直接复制执行的终端命令。',\n            'parameters': {\n              'type': 'object',\n              'required': ['command'],\n              'properties': {\n                'command': {\n                  'type': 'string',\n                  'description': '完整的终端命令，确保可以被粘贴后直接执行。',\n                },\n                'description': {\n                  'type': 'string',\n                  'description': '简述该命令的作用或注意事项。',\n                },\n              },\n            },\n          },\n        },\n      ],\n    };\n  }\n\n  Uri _composeUri(String base, String path) {\n    final sanitizedBase = base.replaceAll(RegExp(r'/+$'), '');\n    final sanitizedPath = path.replaceFirst(RegExp(r'^/+'), '');\n    return Uri.parse('$sanitizedBase/$sanitizedPath');\n  }\n}\n\nclass _ToolCallBuilder {\n  _ToolCallBuilder();\n\n  final StringBuffer arguments = StringBuffer();\n  String? name;\n  bool _emitted = false;\n\n  AskAiCommand? tryBuild({bool force = false}) {\n    if (_emitted && !force) return null;\n    final raw = arguments.toString();\n    try {\n      final decoded = jsonDecode(raw) as Map<String, dynamic>;\n      final command = decoded['command'] as String?;\n      if (command == null || command.trim().isEmpty) {\n        if (force) {\n          _emitted = true;\n        }\n        return null;\n      }\n      final description = decoded['description'] as String? ?? decoded['explanation'] as String? ?? '';\n      _emitted = true;\n      return AskAiCommand(\n        command: command.trim(),\n        description: description.trim(),\n        toolName: name,\n      );\n    } on FormatException {\n      if (force) {\n        _emitted = true;\n      }\n      return null;\n    }\n  }\n}\n\n@immutable\nenum AskAiConfigField { baseUrl, apiKey, model }\n\nclass AskAiConfigException implements Exception {\n  const AskAiConfigException({this.missingFields = const [], this.invalidBaseUrl});\n\n  final List<AskAiConfigField> missingFields;\n  final String? invalidBaseUrl;\n\n  bool get hasInvalidBaseUrl => (invalidBaseUrl ?? '').isNotEmpty;\n\n  @override\n  String toString() {\n    final parts = <String>[];\n    if (missingFields.isNotEmpty) {\n      parts.add('missing: ${missingFields.map((e) => e.name).join(', ')}');\n    }\n    if (hasInvalidBaseUrl) {\n      parts.add('invalidBaseUrl: $invalidBaseUrl');\n    }\n    if (parts.isEmpty) {\n      return 'AskAiConfigException()';\n    }\n    return 'AskAiConfigException(${parts.join('; ')})';\n  }\n}\n\n@immutable\nclass AskAiNetworkException implements Exception {\n  const AskAiNetworkException({required this.message, this.cause});\n\n  final String message;\n  final Object? cause;\n\n  @override\n  String toString() => 'AskAiNetworkException(message: $message)';\n}\n"
  },
  {
    "path": "lib/data/provider/container.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:dartssh2/dartssh2.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/extension/ssh_client.dart';\nimport 'package:server_box/data/model/app/error.dart';\nimport 'package:server_box/data/model/app/scripts/script_consts.dart';\nimport 'package:server_box/data/model/container/image.dart';\nimport 'package:server_box/data/model/container/ps.dart';\nimport 'package:server_box/data/model/container/type.dart';\nimport 'package:server_box/data/res/store.dart';\n\npart 'container.freezed.dart';\npart 'container.g.dart';\n\nfinal _dockerNotFound = RegExp(r\"command not found|Unknown command|Command '\\w+' not found\");\nfinal _podmanEmulationMsg = 'Emulate Docker CLI using podman';\n\n@freezed\nabstract class ContainerState with _$ContainerState {\n  const factory ContainerState({\n    @Default(null) List<ContainerPs>? items,\n    @Default(null) List<ContainerImg>? images,\n    @Default(null) String? version,\n    @Default(null) ContainerErr? error,\n    @Default(null) String? runLog,\n    @Default(ContainerType.docker) ContainerType type,\n    @Default(false) bool isBusy,\n  }) = _ContainerState;\n}\n\n@riverpod\nclass ContainerNotifier extends _$ContainerNotifier {\n  var sudoCompleter = Completer<bool>();\n  String? _cachedPassword;\n\n  @override\n  ContainerState build(SSHClient? client, String userName, String hostId, BuildContext context) {\n    final type = Stores.container.getType(hostId);\n    final initialState = ContainerState(type: type);\n\n    // Async initialization\n    Future.microtask(() => refresh());\n\n    return initialState;\n  }\n\n  Future<String?> _getSudoPassword() async {\n    if (_cachedPassword != null) return _cachedPassword;\n\n    if (!context.mounted) return null;\n    final pwd = await context.showPwdDialog(title: userName, id: hostId);\n\n    if (pwd != null && pwd.isNotEmpty) {\n      _cachedPassword = pwd;\n    }\n    return pwd;\n  }\n\n  Future<void> setType(ContainerType type) async {\n    state = state.copyWith(type: type, error: null, runLog: null, items: null, images: null, version: null);\n    Stores.container.setType(type, hostId);\n    sudoCompleter = Completer<bool>();\n    await refresh();\n  }\n\n  void _requiresSudo() async {\n    /// Podman is rootless\n    if (state.type == ContainerType.podman) return sudoCompleter.complete(false);\n    if (!Stores.setting.containerTrySudo.fetch()) {\n      return sudoCompleter.complete(false);\n    }\n\n    final res = await client?.run(_wrap(ContainerCmdType.images.exec(state.type)));\n    if (res?.string.toLowerCase().contains('permission denied') ?? false) {\n      return sudoCompleter.complete(true);\n    }\n    return sudoCompleter.complete(false);\n  }\n\n  Future<void> refresh({bool isAuto = false}) async {\n    if (state.isBusy) return;\n    state = state.copyWith(isBusy: true);\n\n    if (!sudoCompleter.isCompleted) _requiresSudo();\n\n    final sudo = await sudoCompleter.future;\n\n    /// If sudo is required and auto refresh is enabled, skip the refresh.\n    /// Or this will ask for pwd again and again.\n    if (sudo && isAuto) {\n      state = state.copyWith(isBusy: false);\n      return;\n    }\n\n    String? password;\n    if (sudo) {\n      password = await _getSudoPassword();\n      if (password == null) {\n        state = state.copyWith(\n          isBusy: false,\n          error: ContainerErr(\n            type: ContainerErrType.sudoPasswordRequired,\n            message: l10n.containerSudoPasswordRequired,\n          ),\n        );\n        return;\n      }\n    }\n\n    final includeStats = Stores.setting.containerParseStat.fetch();\n\n    final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats, password: password));\n    int? code;\n    String raw = '';\n    var isPodmanEmulation = false;\n    if (client != null) {\n      (code, raw) = await client!.execWithPwd(\n        cmd,\n        context: context,\n        id: hostId,\n        onStderr: (data, _) {\n          if (data.contains(_podmanEmulationMsg)) {\n            isPodmanEmulation = true;\n          }\n        },\n      );\n    } else {\n      state = state.copyWith(\n        isBusy: false,\n        error: ContainerErr(type: ContainerErrType.noClient),\n      );\n      return;\n    }\n\n    if (!ref.mounted) return;\n    state = state.copyWith(isBusy: false);\n\n    if (!context.mounted) return;\n\n    /// Code 127 means command not found\n    if (code == 127 || raw.contains(_dockerNotFound)) {\n      state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled));\n      return;\n    }\n\n    /// Sudo password error (exitCode = 2)\n    if (code == 2) {\n      _cachedPassword = null;\n      state = state.copyWith(error: ContainerErr(\n        type: ContainerErrType.sudoPasswordIncorrect,\n        message: l10n.containerSudoPasswordIncorrect,\n      ));\n      return;\n    }\n\n    /// Pre-parse Podman detection\n    if (isPodmanEmulation) {\n      state = state.copyWith(\n        error: ContainerErr(\n          type: ContainerErrType.podmanDetected,\n          message: l10n.podmanDockerEmulationDetected,\n        ),\n      );\n      return;\n    }\n\n    /// Detect Podman not installed when using Podman mode\n    if (state.type == ContainerType.podman && raw.contains('podman: not found')) {\n      state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled));\n      return;\n    }\n\n    // Check result segments count\n    final segments = raw.split(ScriptConstants.separator);\n    if (segments.length != ContainerCmdType.values.length) {\n      state = state.copyWith(\n        error: ContainerErr(\n          type: ContainerErrType.segmentsNotMatch,\n          message: 'Container segments: ${segments.length}',\n        ),\n      );\n      Loggers.app.warning('Container segments: ${segments.length}\\n$raw');\n      return;\n    }\n\n    // Parse version\n    final verRaw = ContainerCmdType.version.find(segments);\n    try {\n      final version = json.decode(verRaw)['Client']['Version'];\n      state = state.copyWith(version: version, error: null);\n    } catch (e, trace) {\n      if (state.error == null) {\n        state = state.copyWith(\n          error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'),\n        );\n      }\n      Loggers.app.warning('Container version failed', e, trace);\n    }\n\n    // Parse ps\n    final psRaw = ContainerCmdType.ps.find(segments);\n    try {\n      final lines = psRaw.split('\\n');\n      if (state.type == ContainerType.docker) {\n        /// Due to the fetched data is not in json format, skip table header\n        lines.removeWhere((element) => element.contains('CONTAINER ID'));\n      }\n      lines.removeWhere((element) => element.isEmpty);\n      final items = lines.map((e) => ContainerPs.fromRaw(e, state.type)).toList();\n      state = state.copyWith(items: items);\n    } catch (e, trace) {\n      if (state.error == null) {\n        state = state.copyWith(\n          error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'),\n        );\n      }\n      Loggers.app.warning('Container ps failed', e, trace);\n    }\n\n    // Parse images\n    final imageRaw = ContainerCmdType.images.find(segments).trim();\n    final isEntireJson = imageRaw.startsWith('[') && imageRaw.endsWith(']');\n    try {\n      List<ContainerImg> images;\n      if (isEntireJson) {\n        images = (json.decode(imageRaw) as List)\n            .map((e) => ContainerImg.fromRawJson(json.encode(e), state.type))\n            .toList();\n      } else {\n        final lines = imageRaw.split('\\n');\n        lines.removeWhere((element) => element.isEmpty);\n        images = lines.map((e) => ContainerImg.fromRawJson(e, state.type)).toList();\n      }\n      state = state.copyWith(images: images);\n    } catch (e, trace) {\n      if (state.error == null) {\n        state = state.copyWith(\n          error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'),\n        );\n      }\n      Loggers.app.warning('Container images failed', e, trace);\n    }\n\n    // Parse stats\n    final statsRaw = ContainerCmdType.stats.find(segments);\n    try {\n      final statsLines = statsRaw.split('\\n');\n      statsLines.removeWhere((element) => element.isEmpty);\n      final items = state.items;\n      if (items == null) return;\n\n      for (var item in items) {\n        final id = item.id;\n        if (id == null) continue;\n        if (id.length < 5) continue;\n        final statsLine = statsLines.firstWhereOrNull(\n          /// Use 5 characters to match the container id, possibility of mismatch\n          /// is very low.\n          (element) => element.contains(id.substring(0, 5)),\n        );\n        if (statsLine == null) continue;\n        item.parseStats(statsLine, state.version);\n      }\n    } catch (e, trace) {\n      if (state.error == null) {\n        state = state.copyWith(\n          error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'),\n        );\n      }\n      Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);\n    }\n  }\n\n  Future<ContainerErr?> stop(String id) async => await run('stop $id');\n\n  Future<ContainerErr?> start(String id) async => await run('start $id');\n\n  Future<ContainerErr?> delete(String id, bool force) async {\n    if (force) {\n      return await run('rm -f $id');\n    }\n    return await run('rm $id');\n  }\n\n  Future<ContainerErr?> restart(String id) async => await run('restart $id');\n\n  Future<ContainerErr?> pruneImages({bool all = true}) async {\n    final cmd = 'image prune${all ? \" -a\" : \"\"} -f';\n    return await run(cmd);\n  }\n\n  Future<ContainerErr?> pruneContainers() async {\n    return await run('container prune -f');\n  }\n\n  Future<ContainerErr?> pruneVolumes() async {\n    return await run('volume prune -f');\n  }\n\n  Future<ContainerErr?> pruneSystem() async {\n    return await run('system prune -a -f --volumes');\n  }\n\n  Future<ContainerErr?> run(String cmd, {bool autoRefresh = true}) async {\n    if (client == null) {\n      return ContainerErr(type: ContainerErrType.noClient);\n    }\n\n    cmd = switch (state.type) {\n      ContainerType.docker => 'docker $cmd',\n      ContainerType.podman => 'podman $cmd',\n    };\n\n    final needSudo = await sudoCompleter.future;\n    String? password;\n    if (needSudo) {\n      password = await _getSudoPassword();\n      if (password == null) {\n        return ContainerErr(\n          type: ContainerErrType.sudoPasswordRequired,\n          message: l10n.containerSudoPasswordRequired,\n        );\n      }\n    }\n\n    if (needSudo) {\n      cmd = _buildSudoCmd(cmd, password!);\n    }\n\n    state = state.copyWith(runLog: '');\n    final (code, _) = await client!.execWithPwd(\n      _wrap(cmd),\n      context: context,\n      onStdout: (data, _) {\n        state = state.copyWith(runLog: '${state.runLog}$data');\n      },\n      id: hostId,\n    );\n\n    state = state.copyWith(runLog: null);\n\n    if (code == 2) {\n      _cachedPassword = null;\n      return ContainerErr(\n        type: ContainerErrType.sudoPasswordIncorrect,\n        message: l10n.containerSudoPasswordIncorrect,\n      );\n    }\n    if (code != 0) {\n      return ContainerErr(type: ContainerErrType.unknown, message: 'Command execution failed');\n    }\n    if (autoRefresh) await refresh();\n    return null;\n  }\n\n  /// wrap cmd with `docker host`\n  String _wrap(String cmd) {\n    final dockerHost = Stores.container.fetch(hostId);\n    cmd = 'export LANG=en_US.UTF-8 && $cmd';\n    final noDockerHost = dockerHost?.isEmpty ?? true;\n    if (!noDockerHost) {\n      cmd = 'export DOCKER_HOST=$dockerHost && $cmd';\n    }\n    return cmd;\n  }\n}\n\nconst _jsonFmt = '--format \"{{json .}}\"';\n\nString _buildSudoCmd(String baseCmd, String password) {\n  final pwdBase64 = base64Encode(utf8.encode(password));\n  return 'echo \"$pwdBase64\" | base64 -d | sudo -S $baseCmd';\n}\n\nenum ContainerCmdType {\n  version,\n  ps,\n  stats,\n  images\n  // No specific commands needed for prune actions as they are simple\n  // and don't require splitting output with ScriptConstants.separator\n  ;\n\n  String exec(ContainerType type, {bool includeStats = false}) {\n    final baseCmd = switch (this) {\n      ContainerCmdType.version => '${type.name} version $_jsonFmt',\n      ContainerCmdType.ps => switch (type) {\n        /// TODO: Rollback to json format when performance recovers.\n        /// Use [_jsonFmt] in Docker will cause the operation to slow down.\n        ContainerType.docker =>\n          '${type.name} ps -a --format \"table {{printf \\\\\"'\n              '%-15.15s '\n              '%-30.30s '\n              '${\"%-50.50s \" * 2}\\\\\"'\n              ' .ID .Status .Names .Image}}\"',\n        ContainerType.podman => '${type.name} ps -a $_jsonFmt',\n      },\n      ContainerCmdType.stats => includeStats ? '${type.name} stats --no-stream $_jsonFmt' : 'echo PASS',\n      ContainerCmdType.images => '${type.name} image ls $_jsonFmt',\n    };\n\n    return baseCmd;\n  }\n\n  static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false, String? password}) {\n    final commands = ContainerCmdType.values\n        .map((e) => e.exec(type, includeStats: includeStats))\n        .join('\\necho ${ScriptConstants.separator}\\n');\n\n    final wrappedCommands = 'sh -c \\'${commands.replaceAll(\"'\", \"'\\\\''\")}\\'';\n\n    if (sudo && password != null) {\n      return _buildSudoCmd(wrappedCommands, password);\n    }\n    if (sudo) {\n      return 'sudo -S $wrappedCommands';\n    }\n    return wrappedCommands;\n  }\n\n  /// Find out the required segment from [segments]\n  String find(List<String> segments) {\n    return segments[index];\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/container.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'container.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n/// @nodoc\nmixin _$ContainerState {\n\n List<ContainerPs>? get items; List<ContainerImg>? get images; String? get version; ContainerErr? get error; String? get runLog; ContainerType get type; bool get isBusy;\n/// Create a copy of ContainerState\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$ContainerStateCopyWith<ContainerState> get copyWith => _$ContainerStateCopyWithImpl<ContainerState>(this as ContainerState, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is ContainerState&&const DeepCollectionEquality().equals(other.items, items)&&const DeepCollectionEquality().equals(other.images, images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(items),const DeepCollectionEquality().hash(images),version,error,runLog,type,isBusy);\n\n@override\nString toString() {\n  return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $ContainerStateCopyWith<$Res>  {\n  factory $ContainerStateCopyWith(ContainerState value, $Res Function(ContainerState) _then) = _$ContainerStateCopyWithImpl;\n@useResult\n$Res call({\n List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy\n});\n\n\n\n\n}\n/// @nodoc\nclass _$ContainerStateCopyWithImpl<$Res>\n    implements $ContainerStateCopyWith<$Res> {\n  _$ContainerStateCopyWithImpl(this._self, this._then);\n\n  final ContainerState _self;\n  final $Res Function(ContainerState) _then;\n\n/// Create a copy of ContainerState\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) {\n  return _then(_self.copyWith(\nitems: freezed == items ? _self.items : items // ignore: cast_nullable_to_non_nullable\nas List<ContainerPs>?,images: freezed == images ? _self.images : images // ignore: cast_nullable_to_non_nullable\nas List<ContainerImg>?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable\nas String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable\nas ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable\nas String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable\nas ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable\nas bool,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [ContainerState].\nextension ContainerStatePatterns on ContainerState {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ContainerState value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _ContainerState() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ContainerState value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _ContainerState():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ContainerState value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _ContainerState() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<ContainerPs>? items,  List<ContainerImg>? images,  String? version,  ContainerErr? error,  String? runLog,  ContainerType type,  bool isBusy)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _ContainerState() when $default != null:\nreturn $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<ContainerPs>? items,  List<ContainerImg>? images,  String? version,  ContainerErr? error,  String? runLog,  ContainerType type,  bool isBusy)  $default,) {final _that = this;\nswitch (_that) {\ncase _ContainerState():\nreturn $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<ContainerPs>? items,  List<ContainerImg>? images,  String? version,  ContainerErr? error,  String? runLog,  ContainerType type,  bool isBusy)?  $default,) {final _that = this;\nswitch (_that) {\ncase _ContainerState() when $default != null:\nreturn $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n\nclass _ContainerState implements ContainerState {\n  const _ContainerState({final  List<ContainerPs>? items = null, final  List<ContainerImg>? images = null, this.version = null, this.error = null, this.runLog = null, this.type = ContainerType.docker, this.isBusy = false}): _items = items,_images = images;\n  \n\n final  List<ContainerPs>? _items;\n@override@JsonKey() List<ContainerPs>? get items {\n  final value = _items;\n  if (value == null) return null;\n  if (_items is EqualUnmodifiableListView) return _items;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(value);\n}\n\n final  List<ContainerImg>? _images;\n@override@JsonKey() List<ContainerImg>? get images {\n  final value = _images;\n  if (value == null) return null;\n  if (_images is EqualUnmodifiableListView) return _images;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(value);\n}\n\n@override@JsonKey() final  String? version;\n@override@JsonKey() final  ContainerErr? error;\n@override@JsonKey() final  String? runLog;\n@override@JsonKey() final  ContainerType type;\n@override@JsonKey() final  bool isBusy;\n\n/// Create a copy of ContainerState\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$ContainerStateCopyWith<_ContainerState> get copyWith => __$ContainerStateCopyWithImpl<_ContainerState>(this, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContainerState&&const DeepCollectionEquality().equals(other._items, _items)&&const DeepCollectionEquality().equals(other._images, _images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_items),const DeepCollectionEquality().hash(_images),version,error,runLog,type,isBusy);\n\n@override\nString toString() {\n  return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$ContainerStateCopyWith<$Res> implements $ContainerStateCopyWith<$Res> {\n  factory _$ContainerStateCopyWith(_ContainerState value, $Res Function(_ContainerState) _then) = __$ContainerStateCopyWithImpl;\n@override @useResult\n$Res call({\n List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy\n});\n\n\n\n\n}\n/// @nodoc\nclass __$ContainerStateCopyWithImpl<$Res>\n    implements _$ContainerStateCopyWith<$Res> {\n  __$ContainerStateCopyWithImpl(this._self, this._then);\n\n  final _ContainerState _self;\n  final $Res Function(_ContainerState) _then;\n\n/// Create a copy of ContainerState\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) {\n  return _then(_ContainerState(\nitems: freezed == items ? _self._items : items // ignore: cast_nullable_to_non_nullable\nas List<ContainerPs>?,images: freezed == images ? _self._images : images // ignore: cast_nullable_to_non_nullable\nas List<ContainerImg>?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable\nas String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable\nas ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable\nas String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable\nas ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable\nas bool,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/provider/container.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'container.dart';\n\n// **************************************************************************\n// RiverpodGenerator\n// **************************************************************************\n\n// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, type=warning\n\n@ProviderFor(ContainerNotifier)\nconst containerProvider = ContainerNotifierFamily._();\n\nfinal class ContainerNotifierProvider\n    extends $NotifierProvider<ContainerNotifier, ContainerState> {\n  const ContainerNotifierProvider._({\n    required ContainerNotifierFamily super.from,\n    required (SSHClient?, String, String, BuildContext) super.argument,\n  }) : super(\n         retry: null,\n         name: r'containerProvider',\n         isAutoDispose: true,\n         dependencies: null,\n         $allTransitiveDependencies: null,\n       );\n\n  @override\n  String debugGetCreateSourceHash() => _$containerNotifierHash();\n\n  @override\n  String toString() {\n    return r'containerProvider'\n        ''\n        '$argument';\n  }\n\n  @$internal\n  @override\n  ContainerNotifier create() => ContainerNotifier();\n\n  /// {@macro riverpod.override_with_value}\n  Override overrideWithValue(ContainerState value) {\n    return $ProviderOverride(\n      origin: this,\n      providerOverride: $SyncValueProvider<ContainerState>(value),\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    return other is ContainerNotifierProvider && other.argument == argument;\n  }\n\n  @override\n  int get hashCode {\n    return argument.hashCode;\n  }\n}\n\nString _$containerNotifierHash() => r'85457ec75264199c284572ee45beeaccba2044a1';\n\nfinal class ContainerNotifierFamily extends $Family\n    with\n        $ClassFamilyOverride<\n          ContainerNotifier,\n          ContainerState,\n          ContainerState,\n          ContainerState,\n          (SSHClient?, String, String, BuildContext)\n        > {\n  const ContainerNotifierFamily._()\n    : super(\n        retry: null,\n        name: r'containerProvider',\n        dependencies: null,\n        $allTransitiveDependencies: null,\n        isAutoDispose: true,\n      );\n\n  ContainerNotifierProvider call(\n    SSHClient? client,\n    String userName,\n    String hostId,\n    BuildContext context,\n  ) => ContainerNotifierProvider._(\n    argument: (client, userName, hostId, context),\n    from: this,\n  );\n\n  @override\n  String toString() => r'containerProvider';\n}\n\nabstract class _$ContainerNotifier extends $Notifier<ContainerState> {\n  late final _$args = ref.$arg as (SSHClient?, String, String, BuildContext);\n  SSHClient? get client => _$args.$1;\n  String get userName => _$args.$2;\n  String get hostId => _$args.$3;\n  BuildContext get context => _$args.$4;\n\n  ContainerState build(\n    SSHClient? client,\n    String userName,\n    String hostId,\n    BuildContext context,\n  );\n  @$mustCallSuper\n  @override\n  void runBuild() {\n    final created = build(_$args.$1, _$args.$2, _$args.$3, _$args.$4);\n    final ref = this.ref as $Ref<ContainerState, ContainerState>;\n    final element =\n        ref.element\n            as $ClassProviderElement<\n              AnyNotifier<ContainerState, ContainerState>,\n              ContainerState,\n              Object?,\n              Object?\n            >;\n    element.handleValue(ref, created);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/private_key.dart",
    "content": "import 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\nimport 'package:server_box/core/sync.dart';\nimport 'package:server_box/data/model/server/private_key_info.dart';\nimport 'package:server_box/data/res/store.dart';\n\npart 'private_key.freezed.dart';\npart 'private_key.g.dart';\n\n@freezed\nabstract class PrivateKeyState with _$PrivateKeyState {\n  const factory PrivateKeyState({@Default(<PrivateKeyInfo>[]) List<PrivateKeyInfo> keys}) = _PrivateKeyState;\n}\n\n@Riverpod(keepAlive: true)\nclass PrivateKeyNotifier extends _$PrivateKeyNotifier {\n  @override\n  PrivateKeyState build() {\n    return _load();\n  }\n\n  void reload() {\n    final newState = _load();\n    if (newState == state) return;\n    state = newState;\n  }\n\n  PrivateKeyState _load() {\n    final keys = Stores.key.fetch();\n    return stateOrNull?.copyWith(keys: keys) ?? PrivateKeyState(keys: keys);\n  }\n\n  void add(PrivateKeyInfo info) {\n    final newKeys = [...state.keys, info];\n    state = state.copyWith(keys: newKeys);\n    Stores.key.put(info);\n    bakSync.sync(milliDelay: 1000);\n  }\n\n  void delete(PrivateKeyInfo info) {\n    final newKeys = state.keys.where((e) => e.id != info.id).toList();\n    state = state.copyWith(keys: newKeys);\n    Stores.key.delete(info);\n    bakSync.sync(milliDelay: 1000);\n  }\n\n  void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {\n    final keys = [...state.keys];\n    final idx = keys.indexWhere((e) => e.id == old.id);\n    if (idx == -1) {\n      keys.add(newInfo);\n      Stores.key.put(newInfo);\n      Stores.key.delete(old);\n    } else {\n      keys[idx] = newInfo;\n      Stores.key.put(newInfo);\n    }\n    state = state.copyWith(keys: keys);\n    bakSync.sync(milliDelay: 1000);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/private_key.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'private_key.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n/// @nodoc\nmixin _$PrivateKeyState {\n\n List<PrivateKeyInfo> get keys;\n/// Create a copy of PrivateKeyState\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$PrivateKeyStateCopyWith<PrivateKeyState> get copyWith => _$PrivateKeyStateCopyWithImpl<PrivateKeyState>(this as PrivateKeyState, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is PrivateKeyState&&const DeepCollectionEquality().equals(other.keys, keys));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(keys));\n\n@override\nString toString() {\n  return 'PrivateKeyState(keys: $keys)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $PrivateKeyStateCopyWith<$Res>  {\n  factory $PrivateKeyStateCopyWith(PrivateKeyState value, $Res Function(PrivateKeyState) _then) = _$PrivateKeyStateCopyWithImpl;\n@useResult\n$Res call({\n List<PrivateKeyInfo> keys\n});\n\n\n\n\n}\n/// @nodoc\nclass _$PrivateKeyStateCopyWithImpl<$Res>\n    implements $PrivateKeyStateCopyWith<$Res> {\n  _$PrivateKeyStateCopyWithImpl(this._self, this._then);\n\n  final PrivateKeyState _self;\n  final $Res Function(PrivateKeyState) _then;\n\n/// Create a copy of PrivateKeyState\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? keys = null,}) {\n  return _then(_self.copyWith(\nkeys: null == keys ? _self.keys : keys // ignore: cast_nullable_to_non_nullable\nas List<PrivateKeyInfo>,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [PrivateKeyState].\nextension PrivateKeyStatePatterns on PrivateKeyState {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PrivateKeyState value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _PrivateKeyState() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PrivateKeyState value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _PrivateKeyState():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PrivateKeyState value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _PrivateKeyState() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<PrivateKeyInfo> keys)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _PrivateKeyState() when $default != null:\nreturn $default(_that.keys);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<PrivateKeyInfo> keys)  $default,) {final _that = this;\nswitch (_that) {\ncase _PrivateKeyState():\nreturn $default(_that.keys);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<PrivateKeyInfo> keys)?  $default,) {final _that = this;\nswitch (_that) {\ncase _PrivateKeyState() when $default != null:\nreturn $default(_that.keys);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n\nclass _PrivateKeyState implements PrivateKeyState {\n  const _PrivateKeyState({final  List<PrivateKeyInfo> keys = const <PrivateKeyInfo>[]}): _keys = keys;\n  \n\n final  List<PrivateKeyInfo> _keys;\n@override@JsonKey() List<PrivateKeyInfo> get keys {\n  if (_keys is EqualUnmodifiableListView) return _keys;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(_keys);\n}\n\n\n/// Create a copy of PrivateKeyState\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$PrivateKeyStateCopyWith<_PrivateKeyState> get copyWith => __$PrivateKeyStateCopyWithImpl<_PrivateKeyState>(this, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _PrivateKeyState&&const DeepCollectionEquality().equals(other._keys, _keys));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_keys));\n\n@override\nString toString() {\n  return 'PrivateKeyState(keys: $keys)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$PrivateKeyStateCopyWith<$Res> implements $PrivateKeyStateCopyWith<$Res> {\n  factory _$PrivateKeyStateCopyWith(_PrivateKeyState value, $Res Function(_PrivateKeyState) _then) = __$PrivateKeyStateCopyWithImpl;\n@override @useResult\n$Res call({\n List<PrivateKeyInfo> keys\n});\n\n\n\n\n}\n/// @nodoc\nclass __$PrivateKeyStateCopyWithImpl<$Res>\n    implements _$PrivateKeyStateCopyWith<$Res> {\n  __$PrivateKeyStateCopyWithImpl(this._self, this._then);\n\n  final _PrivateKeyState _self;\n  final $Res Function(_PrivateKeyState) _then;\n\n/// Create a copy of PrivateKeyState\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? keys = null,}) {\n  return _then(_PrivateKeyState(\nkeys: null == keys ? _self._keys : keys // ignore: cast_nullable_to_non_nullable\nas List<PrivateKeyInfo>,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/provider/private_key.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'private_key.dart';\n\n// **************************************************************************\n// RiverpodGenerator\n// **************************************************************************\n\n// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, type=warning\n\n@ProviderFor(PrivateKeyNotifier)\nconst privateKeyProvider = PrivateKeyNotifierProvider._();\n\nfinal class PrivateKeyNotifierProvider\n    extends $NotifierProvider<PrivateKeyNotifier, PrivateKeyState> {\n  const PrivateKeyNotifierProvider._()\n    : super(\n        from: null,\n        argument: null,\n        retry: null,\n        name: r'privateKeyProvider',\n        isAutoDispose: false,\n        dependencies: null,\n        $allTransitiveDependencies: null,\n      );\n\n  @override\n  String debugGetCreateSourceHash() => _$privateKeyNotifierHash();\n\n  @$internal\n  @override\n  PrivateKeyNotifier create() => PrivateKeyNotifier();\n\n  /// {@macro riverpod.override_with_value}\n  Override overrideWithValue(PrivateKeyState value) {\n    return $ProviderOverride(\n      origin: this,\n      providerOverride: $SyncValueProvider<PrivateKeyState>(value),\n    );\n  }\n}\n\nString _$privateKeyNotifierHash() =>\n    r'12edd05dca29d1cbc9e2a3e047c3d417d22f7bb7';\n\nabstract class _$PrivateKeyNotifier extends $Notifier<PrivateKeyState> {\n  PrivateKeyState build();\n  @$mustCallSuper\n  @override\n  void runBuild() {\n    final created = build();\n    final ref = this.ref as $Ref<PrivateKeyState, PrivateKeyState>;\n    final element =\n        ref.element\n            as $ClassProviderElement<\n              AnyNotifier<PrivateKeyState, PrivateKeyState>,\n              PrivateKeyState,\n              Object?,\n              Object?\n            >;\n    element.handleValue(ref, created);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/providers.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:flutter_riverpod/misc.dart';\n\nimport 'package:server_box/data/provider/private_key.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/provider/sftp.dart';\nimport 'package:server_box/data/provider/snippet.dart';\n\n/// !library;\n/// ref.useNotifier, ref.readProvider, ref.watchProvider\n/// \n/// Usage:\n/// - `providers.read.server` -> `ref.read(serversProvider)`\n/// - `providers.use.snippet` -> `ref.read(snippetsNotifierProvider.notifier)`\n\nextension RiverpodNotifiers on ConsumerState {\n  T useNotifier<T extends Notifier<Object?>>(NotifierProvider<T, Object?> provider) {\n    return ref.read(provider.notifier);\n  }\n\n  T readProvider<T>(ProviderBase<T> provider) {\n    return ref.read(provider);\n  }\n\n  T watchProvider<T>(ProviderBase<T> provider) {\n    return ref.watch(provider);\n  }\n\n  MyProviders get providers => MyProviders(ref);\n}\n\nfinal class MyProviders {\n  final WidgetRef ref;\n  const MyProviders(this.ref);\n\n  ReadMyProvider get read => ReadMyProvider(ref);\n  WatchMyProvider get watch => WatchMyProvider(ref);\n  UseNotifierMyProvider get use => UseNotifierMyProvider(ref);\n}\n\nfinal class ReadMyProvider {\n  final WidgetRef ref;\n  const ReadMyProvider(this.ref);\n\n  T call<T>(ProviderBase<T> provider) => ref.read(provider);\n  \n  // Specific provider getters\n  ServersState get server => ref.read(serversProvider);\n  SnippetState get snippet => ref.read(snippetProvider);\n  AppState get app => ref.read(appStatesProvider);\n  PrivateKeyState get privateKey => ref.read(privateKeyProvider);\n  SftpState get sftp => ref.read(sftpProvider);\n}\n\nfinal class WatchMyProvider {\n  final WidgetRef ref;\n  const WatchMyProvider(this.ref);\n\n  T call<T>(ProviderBase<T> provider) => ref.watch(provider);\n  \n  // Specific provider getters\n  ServersState get server => ref.watch(serversProvider);\n  SnippetState get snippet => ref.watch(snippetProvider);\n  AppState get app => ref.watch(appStatesProvider);\n  PrivateKeyState get privateKey => ref.watch(privateKeyProvider);\n  SftpState get sftp => ref.watch(sftpProvider);\n}\n\nfinal class UseNotifierMyProvider {\n  final WidgetRef ref;\n  const UseNotifierMyProvider(this.ref);\n\n  T call<T extends Notifier<Object?>>(NotifierProvider<T, Object?> provider) =>\n      ref.read(provider.notifier);\n  \n  // Specific provider notifier getters\n  ServersNotifier get server => ref.read(serversProvider.notifier);\n  SnippetNotifier get snippet => ref.read(snippetProvider.notifier);\n  AppStates get app => ref.read(appStatesProvider.notifier);\n  PrivateKeyNotifier get privateKey => ref.read(privateKeyProvider.notifier);\n  SftpNotifier get sftp => ref.read(sftpProvider.notifier);\n}"
  },
  {
    "path": "lib/data/provider/pve.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:computer/computer.dart';\nimport 'package:dartssh2/dartssh2.dart';\nimport 'package:dio/dio.dart';\nimport 'package:dio/io.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/app/error.dart';\nimport 'package:server_box/data/model/server/pve.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/provider/server/single.dart';\n\npart 'pve.freezed.dart';\npart 'pve.g.dart';\n\ntypedef PveCtrlFunc = Future<bool> Function(String node, String id);\n\n@freezed\nabstract class PveState with _$PveState {\n  const factory PveState({\n    @Default(null) PveErr? error,\n    @Default(null) PveRes? data,\n    @Default(null) String? release,\n    @Default(false) bool isBusy,\n    @Default(false) bool isConnected,\n  }) = _PveState;\n}\n\n@riverpod\nclass PveNotifier extends _$PveNotifier {\n  late final Spi spi;\n  late String addr;\n  late final SSHClient _client;\n  late final ServerSocket _serverSocket;\n  final List<SSHForwardChannel> _forwards = [];\n  int _localPort = 0;\n  late final Dio session;\n  late final bool _ignoreCert;\n\n  @override\n  PveState build(Spi spiParam) {\n    spi = spiParam;\n    final serverState = ref.watch(serverProvider(spi.id));\n    final client = serverState.client;\n    if (client == null) {\n      return const PveState(error: PveErr(type: PveErrType.net, message: 'Server client is null'));\n    }\n    _client = client;\n    final addr = spi.custom?.pveAddr;\n    if (addr == null) {\n      return PveState(error: PveErr(type: PveErrType.net, message: 'PVE address is null'));\n    }\n    this.addr = addr;\n    _ignoreCert = spi.custom?.pveIgnoreCert ?? false;\n    _initSession();\n    // Async initialization\n    Future.microtask(() => _init());\n    return const PveState();\n  }\n\n  void _initSession() {\n    session = Dio()\n      ..httpClientAdapter = IOHttpClientAdapter(\n        createHttpClient: () {\n          final client = HttpClient();\n          client.connectionFactory = cf;\n          if (_ignoreCert) {\n            client.badCertificateCallback = (_, _, _) => true;\n          }\n          return client;\n        },\n        validateCertificate: _ignoreCert ? (_, _, _) => true : null,\n      );\n  }\n\n  bool get onlyOneNode => state.data?.nodes.length == 1;\n\n  Future<void> _init() async {\n    try {\n      await _forward();\n      await _login();\n      await _getRelease();\n      state = state.copyWith(isConnected: true);\n    } on PveErr {\n      state = state.copyWith(error: PveErr(type: PveErrType.loginFailed, message: l10n.pveLoginFailed));\n    } catch (e, s) {\n      Loggers.app.warning('PVE init failed', e, s);\n      state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString()));\n    }\n  }\n\n  Future<void> _forward() async {\n    final url = Uri.parse(addr);\n    if (_localPort == 0) {\n      _serverSocket = await ServerSocket.bind('localhost', 0);\n      _localPort = _serverSocket.port;\n      _serverSocket.listen((socket) async {\n        final forward = await _client.forwardLocal(url.host, url.port);\n        _forwards.add(forward);\n        forward.stream.cast<List<int>>().pipe(socket);\n        socket.cast<List<int>>().pipe(forward.sink);\n      });\n      final newUrl = Uri.parse(\n        addr,\n      ).replace(host: 'localhost', port: _localPort).toString();\n      dprint('Forwarding $newUrl to $addr');\n    }\n  }\n\n  Future<ConnectionTask<Socket>> cf(\n    Uri url,\n    String? proxyHost,\n    int? proxyPort,\n  ) async {\n    /* final serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0);\n    final _localPort = serverSocket.port;\n    serverSocket.listen((socket) async {\n      final forward = await _client.forwardLocal(url.host, url.port);\n      forwards.add(forward);\n      forward.stream.cast<List<int>>().pipe(socket);\n      socket.cast<List<int>>().pipe(forward.sink);\n    });*/\n\n    if (url.isScheme('https')) {\n      return SecureSocket.startConnect(\n        'localhost',\n        _localPort,\n        onBadCertificate: (_) => true,\n      );\n    } else {\n      return Socket.startConnect('localhost', _localPort);\n    }\n  }\n\n  Future<void> _login() async {\n    final resp = await session.post(\n      '$addr/api2/extjs/access/ticket',\n      data: {\n        'username': spi.user,\n        'password': spi.pwd,\n        'realm': 'pam',\n        'new-format': '1',\n      },\n      options: Options(\n        headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType},\n      ),\n    );\n    try {\n      final ticket = resp.data['data']['ticket'];\n      session.options.headers['CSRFPreventionToken'] =\n          resp.data['data']['CSRFPreventionToken'];\n      session.options.headers['Cookie'] = 'PVEAuthCookie=$ticket';\n    } catch (e) {\n      throw PveErr(type: PveErrType.loginFailed, message: e.toString());\n    }\n  }\n\n  /// Returns true if the PVE version is 8.0 or later\n  Future<void> _getRelease() async {\n    final resp = await session.get('$addr/api2/extjs/version');\n    final version = resp.data['data']['release'] as String?;\n    if (version != null) {\n      state = state.copyWith(release: version);\n    }\n  }\n\n  Future<void> list() async {\n    if (!state.isConnected || state.isBusy) return;\n    state = state.copyWith(isBusy: true);\n    try {\n      final resp = await session.get('$addr/api2/json/cluster/resources');\n      final res = resp.data['data'] as List;\n      final result = await Computer.shared.start(PveRes.parse, (\n        res,\n        state.data,\n      ));\n      state = state.copyWith(data: result, error: null);\n    } catch (e) {\n      Loggers.app.warning('PVE list failed', e);\n      state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString()));\n    } finally {\n      state = state.copyWith(isBusy: false);\n    }\n  }\n\n  Future<bool> reboot(String node, String id) async {\n    if (!state.isConnected) return false;\n    final resp = await session.post(\n      '$addr/api2/json/nodes/$node/$id/status/reboot',\n    );\n    final success = _isCtrlSuc(resp);\n    if (success) await list(); // Refresh data\n    return success;\n  }\n\n  Future<bool> start(String node, String id) async {\n    if (!state.isConnected) return false;\n    final resp = await session.post(\n      '$addr/api2/json/nodes/$node/$id/status/start',\n    );\n    final success = _isCtrlSuc(resp);\n    if (success) await list(); // Refresh data\n    return success;\n  }\n\n  Future<bool> stop(String node, String id) async {\n    if (!state.isConnected) return false;\n    final resp = await session.post(\n      '$addr/api2/json/nodes/$node/$id/status/stop',\n    );\n    final success = _isCtrlSuc(resp);\n    if (success) await list(); // Refresh data\n    return success;\n  }\n\n  Future<bool> shutdown(String node, String id) async {\n    if (!state.isConnected) return false;\n    final resp = await session.post(\n      '$addr/api2/json/nodes/$node/$id/status/shutdown',\n    );\n    final success = _isCtrlSuc(resp);\n    if (success) await list(); // Refresh data\n    return success;\n  }\n\n  bool _isCtrlSuc(Response resp) {\n    return resp.statusCode == 200;\n  }\n\n  Future<void> dispose() async {\n    try {\n      await _serverSocket.close();\n    } catch (e, s) {\n      Loggers.app.warning('Failed to close server socket', e, s);\n    }\n    for (final forward in _forwards) {\n      try {\n        forward.close();\n      } catch (e, s) {\n        Loggers.app.warning('Failed to close forward', e, s);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/pve.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'pve.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n/// @nodoc\nmixin _$PveState {\n\n PveErr? get error; PveRes? get data; String? get release; bool get isBusy; bool get isConnected;\n/// Create a copy of PveState\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$PveStateCopyWith<PveState> get copyWith => _$PveStateCopyWithImpl<PveState>(this as PveState, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected);\n\n@override\nString toString() {\n  return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $PveStateCopyWith<$Res>  {\n  factory $PveStateCopyWith(PveState value, $Res Function(PveState) _then) = _$PveStateCopyWithImpl;\n@useResult\n$Res call({\n PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected\n});\n\n\n\n\n}\n/// @nodoc\nclass _$PveStateCopyWithImpl<$Res>\n    implements $PveStateCopyWith<$Res> {\n  _$PveStateCopyWithImpl(this._self, this._then);\n\n  final PveState _self;\n  final $Res Function(PveState) _then;\n\n/// Create a copy of PveState\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) {\n  return _then(_self.copyWith(\nerror: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable\nas PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable\nas PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable\nas String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable\nas bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable\nas bool,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [PveState].\nextension PveStatePatterns on PveState {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PveState value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _PveState() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PveState value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _PveState():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PveState value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _PveState() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( PveErr? error,  PveRes? data,  String? release,  bool isBusy,  bool isConnected)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _PveState() when $default != null:\nreturn $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( PveErr? error,  PveRes? data,  String? release,  bool isBusy,  bool isConnected)  $default,) {final _that = this;\nswitch (_that) {\ncase _PveState():\nreturn $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( PveErr? error,  PveRes? data,  String? release,  bool isBusy,  bool isConnected)?  $default,) {final _that = this;\nswitch (_that) {\ncase _PveState() when $default != null:\nreturn $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n\nclass _PveState implements PveState {\n  const _PveState({this.error = null, this.data = null, this.release = null, this.isBusy = false, this.isConnected = false});\n  \n\n@override@JsonKey() final  PveErr? error;\n@override@JsonKey() final  PveRes? data;\n@override@JsonKey() final  String? release;\n@override@JsonKey() final  bool isBusy;\n@override@JsonKey() final  bool isConnected;\n\n/// Create a copy of PveState\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$PveStateCopyWith<_PveState> get copyWith => __$PveStateCopyWithImpl<_PveState>(this, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected);\n\n@override\nString toString() {\n  return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$PveStateCopyWith<$Res> implements $PveStateCopyWith<$Res> {\n  factory _$PveStateCopyWith(_PveState value, $Res Function(_PveState) _then) = __$PveStateCopyWithImpl;\n@override @useResult\n$Res call({\n PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected\n});\n\n\n\n\n}\n/// @nodoc\nclass __$PveStateCopyWithImpl<$Res>\n    implements _$PveStateCopyWith<$Res> {\n  __$PveStateCopyWithImpl(this._self, this._then);\n\n  final _PveState _self;\n  final $Res Function(_PveState) _then;\n\n/// Create a copy of PveState\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) {\n  return _then(_PveState(\nerror: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable\nas PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable\nas PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable\nas String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable\nas bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable\nas bool,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/provider/pve.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'pve.dart';\n\n// **************************************************************************\n// RiverpodGenerator\n// **************************************************************************\n\n// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, type=warning\n\n@ProviderFor(PveNotifier)\nconst pveProvider = PveNotifierFamily._();\n\nfinal class PveNotifierProvider\n    extends $NotifierProvider<PveNotifier, PveState> {\n  const PveNotifierProvider._({\n    required PveNotifierFamily super.from,\n    required Spi super.argument,\n  }) : super(\n         retry: null,\n         name: r'pveProvider',\n         isAutoDispose: true,\n         dependencies: null,\n         $allTransitiveDependencies: null,\n       );\n\n  @override\n  String debugGetCreateSourceHash() => _$pveNotifierHash();\n\n  @override\n  String toString() {\n    return r'pveProvider'\n        ''\n        '($argument)';\n  }\n\n  @$internal\n  @override\n  PveNotifier create() => PveNotifier();\n\n  /// {@macro riverpod.override_with_value}\n  Override overrideWithValue(PveState value) {\n    return $ProviderOverride(\n      origin: this,\n      providerOverride: $SyncValueProvider<PveState>(value),\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    return other is PveNotifierProvider && other.argument == argument;\n  }\n\n  @override\n  int get hashCode {\n    return argument.hashCode;\n  }\n}\n\nString _$pveNotifierHash() => r'1e71faadee074b9c07bee731ef4ae6505e791967';\n\nfinal class PveNotifierFamily extends $Family\n    with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> {\n  const PveNotifierFamily._()\n    : super(\n        retry: null,\n        name: r'pveProvider',\n        dependencies: null,\n        $allTransitiveDependencies: null,\n        isAutoDispose: true,\n      );\n\n  PveNotifierProvider call(Spi spiParam) =>\n      PveNotifierProvider._(argument: spiParam, from: this);\n\n  @override\n  String toString() => r'pveProvider';\n}\n\nabstract class _$PveNotifier extends $Notifier<PveState> {\n  late final _$args = ref.$arg as Spi;\n  Spi get spiParam => _$args;\n\n  PveState build(Spi spiParam);\n  @$mustCallSuper\n  @override\n  void runBuild() {\n    final created = build(_$args);\n    final ref = this.ref as $Ref<PveState, PveState>;\n    final element =\n        ref.element\n            as $ClassProviderElement<\n              AnyNotifier<PveState, PveState>,\n              PveState,\n              Object?,\n              Object?\n            >;\n    element.handleValue(ref, created);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/server/all.dart",
    "content": "import 'dart:async';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\nimport 'package:server_box/core/sync.dart';\nimport 'package:server_box/data/model/server/server.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/try_limiter.dart';\nimport 'package:server_box/data/provider/server/single.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/data/ssh/session_manager.dart';\n\npart 'all.freezed.dart';\npart 'all.g.dart';\n\n@freezed\nabstract class ServersState with _$ServersState {\n  const factory ServersState({\n    @Default({}) Map<String, Spi> servers,\n    @Default([]) List<String> serverOrder,\n    @Default(<String>{}) Set<String> tags,\n    @Default(<String>{}) Set<String> manualDisconnectedIds,\n    Timer? autoRefreshTimer,\n  }) = _ServersState;\n}\n\n@Riverpod(keepAlive: true)\nclass ServersNotifier extends _$ServersNotifier {\n  @override\n  ServersState build() {\n    return _load();\n  }\n\n  Future<void> reload() async {\n    final newState = _load();\n    if (newState == state) return;\n    state = newState;\n    await refresh();\n  }\n\n  ServersState _load() {\n    final spis = Stores.server.fetch();\n    final newServers = <String, Spi>{};\n    final newServerOrder = <String>[];\n\n    for (final spi in spis) {\n      newServers[spi.id] = spi;\n    }\n\n    final serverOrder_ = Stores.setting.serverOrder.fetch();\n    if (serverOrder_.isNotEmpty) {\n      spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id);\n      newServerOrder.addAll(spis.map((e) => e.id));\n    } else {\n      newServerOrder.addAll(newServers.keys);\n    }\n\n    // Must use [equals] to compare [Order] here.\n    if (!newServerOrder.equals(serverOrder_)) {\n      Stores.setting.serverOrder.put(newServerOrder);\n    }\n\n    final newTags = _calculateTags(newServers);\n\n    return stateOrNull?.copyWith(servers: newServers, serverOrder: newServerOrder, tags: newTags) ??\n        ServersState(servers: newServers, serverOrder: newServerOrder, tags: newTags);\n  }\n\n  Set<String> _calculateTags(Map<String, Spi> servers) {\n    final tags = <String>{};\n    for (final spi in servers.values) {\n      final spiTags = spi.tags;\n      if (spiTags == null) continue;\n      for (final t in spiTags) {\n        tags.add(t);\n      }\n    }\n    return tags;\n  }\n\n  /// Get a [Spi] by [spi] or [id].\n  ///\n  /// Priority: [spi] > [id]\n  Spi? pick({Spi? spi, String? id}) {\n    if (spi != null) {\n      return state.servers[spi.id];\n    }\n    if (id != null) {\n      return state.servers[id];\n    }\n    return null;\n  }\n\n  /// if [spi] is specificed then only refresh this server\n  /// [onlyFailed] only refresh failed servers\n  Future<void> refresh({Spi? spi, bool onlyFailed = false}) async {\n    if (spi != null) {\n      final newManualDisconnected = Set<String>.from(state.manualDisconnectedIds)..remove(spi.id);\n      state = state.copyWith(manualDisconnectedIds: newManualDisconnected);\n      final serverNotifier = ref.read(serverProvider(spi.id).notifier);\n      await serverNotifier.refresh();\n      return;\n    }\n\n    final serversToRefresh = <MapEntry<String, Spi>>[];\n    final idsToResetLimiter = <String>[];\n\n    for (final entry in state.servers.entries) {\n      final serverId = entry.key;\n      final spi = entry.value;\n\n      if (state.manualDisconnectedIds.contains(serverId)) continue;\n\n      final serverState = ref.read(serverProvider(serverId));\n\n      if (onlyFailed) {\n        if (serverState.conn != ServerConn.failed) continue;\n        idsToResetLimiter.add(serverId);\n      }\n\n      if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) continue;\n\n      serversToRefresh.add(entry);\n    }\n\n    for (final id in idsToResetLimiter) {\n      TryLimiter.reset(id);\n    }\n\n    for (final entry in serversToRefresh) {\n      final serverNotifier = ref.read(serverProvider(entry.key).notifier);\n      serverNotifier.refresh().ignore();\n    }\n  }\n\n  Future<void> startAutoRefresh() async {\n    var duration = Stores.setting.serverStatusUpdateInterval.fetch();\n    stopAutoRefresh();\n    if (duration == 0) return;\n    if (duration <= 1 || duration > 10) {\n      Loggers.app.warning('Invalid duration: $duration, use default 3');\n      duration = 3;\n    }\n    final timer = Timer.periodic(Duration(seconds: duration), (_) async {\n      await refresh();\n    });\n    state = state.copyWith(autoRefreshTimer: timer);\n  }\n\n  void stopAutoRefresh() {\n    final timer = state.autoRefreshTimer;\n    if (timer != null) {\n      timer.cancel();\n    }\n    state = state.copyWith(autoRefreshTimer: null);\n  }\n\n  bool get isAutoRefreshOn => state.autoRefreshTimer != null;\n\n  void setDisconnected() {\n    for (final serverId in state.servers.keys) {\n      final serverNotifier = ref.read(serverProvider(serverId).notifier);\n      serverNotifier.updateConnection(ServerConn.disconnected);\n\n      // Update SSH session status to disconnected\n      final sessionId = 'ssh_$serverId';\n      TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);\n    }\n    //TryLimiter.clear();\n  }\n\n  void closeServer({String? id}) {\n    if (id == null) {\n      for (final serverId in state.servers.keys) {\n        closeOneServer(serverId);\n      }\n      return;\n    }\n    closeOneServer(id);\n  }\n\n  void closeOneServer(String id) {\n    final spi = state.servers[id];\n    if (spi == null) {\n      Loggers.app.warning('Server with id $id not found');\n      return;\n    }\n\n    final serverNotifier = ref.read(serverProvider(id).notifier);\n    serverNotifier.closeConnection();\n\n    final newManualDisconnected = Set<String>.from(state.manualDisconnectedIds)..add(id);\n    state = state.copyWith(manualDisconnectedIds: newManualDisconnected);\n\n    // Remove SSH session when server is manually closed\n    final sessionId = 'ssh_$id';\n    TermSessionManager.remove(sessionId);\n  }\n\n  void addServer(Spi spi) {\n    final newServers = Map<String, Spi>.from(state.servers);\n    newServers[spi.id] = spi;\n\n    final newOrder = List<String>.from(state.serverOrder)..add(spi.id);\n    final newTags = _calculateTags(newServers);\n\n    state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags);\n\n    Stores.server.put(spi);\n    Stores.setting.serverOrder.put(newOrder);\n    refresh(spi: spi);\n    bakSync.sync(milliDelay: 1000);\n  }\n\n  void delServer(String id) {\n    final newServers = Map<String, Spi>.from(state.servers);\n    newServers.remove(id);\n\n    final newOrder = List<String>.from(state.serverOrder)..remove(id);\n    final newTags = _calculateTags(newServers);\n\n    state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags);\n\n    Stores.setting.serverOrder.put(newOrder);\n    Stores.server.delete(id);\n\n    // Remove SSH session when server is deleted\n    final sessionId = 'ssh_$id';\n    TermSessionManager.remove(sessionId);\n\n    bakSync.sync(milliDelay: 1000);\n  }\n\n  void deleteAll() {\n    // Remove all SSH sessions before clearing servers\n    for (final id in state.servers.keys) {\n      final sessionId = 'ssh_$id';\n      TermSessionManager.remove(sessionId);\n    }\n\n    state = const ServersState();\n\n    Stores.setting.serverOrder.put([]);\n    Stores.server.clear();\n    bakSync.sync(milliDelay: 1000);\n  }\n\n  void updateServerOrder(List<String> order) {\n    final seen = <String>{};\n    final newOrder = <String>[];\n\n    for (final id in order) {\n      if (!state.servers.containsKey(id)) {\n        continue;\n      }\n      if (!seen.add(id)) {\n        continue;\n      }\n      newOrder.add(id);\n    }\n\n    for (final id in state.servers.keys) {\n      if (seen.add(id)) {\n        newOrder.add(id);\n      }\n    }\n\n    if (_isSameOrder(newOrder, state.serverOrder)) {\n      return;\n    }\n\n    state = state.copyWith(serverOrder: newOrder);\n    Stores.setting.serverOrder.put(newOrder);\n    bakSync.sync(milliDelay: 1000);\n  }\n\n  bool _isSameOrder(List<String> a, List<String> b) {\n    if (identical(a, b)) {\n      return true;\n    }\n    if (a.length != b.length) {\n      return false;\n    }\n    for (var i = 0; i < a.length; i++) {\n      if (a[i] != b[i]) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  Future<void> updateServer(Spi old, Spi newSpi) async {\n    if (old != newSpi) {\n      Stores.server.update(old, newSpi);\n\n      final newServers = Map<String, Spi>.from(state.servers);\n      final newOrder = List<String>.from(state.serverOrder);\n\n      if (newSpi.id != old.id) {\n        newServers[newSpi.id] = newSpi;\n        newServers.remove(old.id);\n        newOrder.update(old.id, newSpi.id);\n        Stores.setting.serverOrder.put(newOrder);\n\n        // Update SSH session ID when server ID changes\n        final oldSessionId = 'ssh_${old.id}';\n        TermSessionManager.remove(oldSessionId);\n        // Session will be re-added when reconnecting if necessary\n      } else {\n        newServers[old.id] = newSpi;\n        // Update SPI in the corresponding IndividualServerNotifier\n        final serverNotifier = ref.read(serverProvider(old.id).notifier);\n        serverNotifier.updateSpi(newSpi);\n      }\n\n      final newTags = _calculateTags(newServers);\n      state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags);\n\n      // Only reconnect if neccessary\n      if (newSpi.shouldReconnect(old)) {\n        // Use [newSpi.id] instead of [old.id] because [old.id] may be changed\n        TryLimiter.reset(newSpi.id);\n        refresh(spi: newSpi);\n      }\n    }\n    bakSync.sync(milliDelay: 1000);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/server/all.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'all.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n/// @nodoc\nmixin _$ServersState {\n\n Map<String, Spi> get servers; List<String> get serverOrder; Set<String> get tags; Set<String> get manualDisconnectedIds; Timer? get autoRefreshTimer;\n/// Create a copy of ServersState\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$ServersStateCopyWith<ServersState> get copyWith => _$ServersStateCopyWithImpl<ServersState>(this as ServersState, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is ServersState&&const DeepCollectionEquality().equals(other.servers, servers)&&const DeepCollectionEquality().equals(other.serverOrder, serverOrder)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.manualDisconnectedIds, manualDisconnectedIds)&&(identical(other.autoRefreshTimer, autoRefreshTimer) || other.autoRefreshTimer == autoRefreshTimer));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(servers),const DeepCollectionEquality().hash(serverOrder),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(manualDisconnectedIds),autoRefreshTimer);\n\n@override\nString toString() {\n  return 'ServersState(servers: $servers, serverOrder: $serverOrder, tags: $tags, manualDisconnectedIds: $manualDisconnectedIds, autoRefreshTimer: $autoRefreshTimer)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $ServersStateCopyWith<$Res>  {\n  factory $ServersStateCopyWith(ServersState value, $Res Function(ServersState) _then) = _$ServersStateCopyWithImpl;\n@useResult\n$Res call({\n Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer\n});\n\n\n\n\n}\n/// @nodoc\nclass _$ServersStateCopyWithImpl<$Res>\n    implements $ServersStateCopyWith<$Res> {\n  _$ServersStateCopyWithImpl(this._self, this._then);\n\n  final ServersState _self;\n  final $Res Function(ServersState) _then;\n\n/// Create a copy of ServersState\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? servers = null,Object? serverOrder = null,Object? tags = null,Object? manualDisconnectedIds = null,Object? autoRefreshTimer = freezed,}) {\n  return _then(_self.copyWith(\nservers: null == servers ? _self.servers : servers // ignore: cast_nullable_to_non_nullable\nas Map<String, Spi>,serverOrder: null == serverOrder ? _self.serverOrder : serverOrder // ignore: cast_nullable_to_non_nullable\nas List<String>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable\nas Set<String>,manualDisconnectedIds: null == manualDisconnectedIds ? _self.manualDisconnectedIds : manualDisconnectedIds // ignore: cast_nullable_to_non_nullable\nas Set<String>,autoRefreshTimer: freezed == autoRefreshTimer ? _self.autoRefreshTimer : autoRefreshTimer // ignore: cast_nullable_to_non_nullable\nas Timer?,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [ServersState].\nextension ServersStatePatterns on ServersState {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServersState value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _ServersState() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServersState value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _ServersState():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServersState value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _ServersState() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Map<String, Spi> servers,  List<String> serverOrder,  Set<String> tags,  Set<String> manualDisconnectedIds,  Timer? autoRefreshTimer)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _ServersState() when $default != null:\nreturn $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Map<String, Spi> servers,  List<String> serverOrder,  Set<String> tags,  Set<String> manualDisconnectedIds,  Timer? autoRefreshTimer)  $default,) {final _that = this;\nswitch (_that) {\ncase _ServersState():\nreturn $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Map<String, Spi> servers,  List<String> serverOrder,  Set<String> tags,  Set<String> manualDisconnectedIds,  Timer? autoRefreshTimer)?  $default,) {final _that = this;\nswitch (_that) {\ncase _ServersState() when $default != null:\nreturn $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n\nclass _ServersState implements ServersState {\n  const _ServersState({final  Map<String, Spi> servers = const {}, final  List<String> serverOrder = const [], final  Set<String> tags = const <String>{}, final  Set<String> manualDisconnectedIds = const <String>{}, this.autoRefreshTimer}): _servers = servers,_serverOrder = serverOrder,_tags = tags,_manualDisconnectedIds = manualDisconnectedIds;\n  \n\n final  Map<String, Spi> _servers;\n@override@JsonKey() Map<String, Spi> get servers {\n  if (_servers is EqualUnmodifiableMapView) return _servers;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableMapView(_servers);\n}\n\n final  List<String> _serverOrder;\n@override@JsonKey() List<String> get serverOrder {\n  if (_serverOrder is EqualUnmodifiableListView) return _serverOrder;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(_serverOrder);\n}\n\n final  Set<String> _tags;\n@override@JsonKey() Set<String> get tags {\n  if (_tags is EqualUnmodifiableSetView) return _tags;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableSetView(_tags);\n}\n\n final  Set<String> _manualDisconnectedIds;\n@override@JsonKey() Set<String> get manualDisconnectedIds {\n  if (_manualDisconnectedIds is EqualUnmodifiableSetView) return _manualDisconnectedIds;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableSetView(_manualDisconnectedIds);\n}\n\n@override final  Timer? autoRefreshTimer;\n\n/// Create a copy of ServersState\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$ServersStateCopyWith<_ServersState> get copyWith => __$ServersStateCopyWithImpl<_ServersState>(this, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServersState&&const DeepCollectionEquality().equals(other._servers, _servers)&&const DeepCollectionEquality().equals(other._serverOrder, _serverOrder)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._manualDisconnectedIds, _manualDisconnectedIds)&&(identical(other.autoRefreshTimer, autoRefreshTimer) || other.autoRefreshTimer == autoRefreshTimer));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_servers),const DeepCollectionEquality().hash(_serverOrder),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_manualDisconnectedIds),autoRefreshTimer);\n\n@override\nString toString() {\n  return 'ServersState(servers: $servers, serverOrder: $serverOrder, tags: $tags, manualDisconnectedIds: $manualDisconnectedIds, autoRefreshTimer: $autoRefreshTimer)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$ServersStateCopyWith<$Res> implements $ServersStateCopyWith<$Res> {\n  factory _$ServersStateCopyWith(_ServersState value, $Res Function(_ServersState) _then) = __$ServersStateCopyWithImpl;\n@override @useResult\n$Res call({\n Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer\n});\n\n\n\n\n}\n/// @nodoc\nclass __$ServersStateCopyWithImpl<$Res>\n    implements _$ServersStateCopyWith<$Res> {\n  __$ServersStateCopyWithImpl(this._self, this._then);\n\n  final _ServersState _self;\n  final $Res Function(_ServersState) _then;\n\n/// Create a copy of ServersState\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? servers = null,Object? serverOrder = null,Object? tags = null,Object? manualDisconnectedIds = null,Object? autoRefreshTimer = freezed,}) {\n  return _then(_ServersState(\nservers: null == servers ? _self._servers : servers // ignore: cast_nullable_to_non_nullable\nas Map<String, Spi>,serverOrder: null == serverOrder ? _self._serverOrder : serverOrder // ignore: cast_nullable_to_non_nullable\nas List<String>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable\nas Set<String>,manualDisconnectedIds: null == manualDisconnectedIds ? _self._manualDisconnectedIds : manualDisconnectedIds // ignore: cast_nullable_to_non_nullable\nas Set<String>,autoRefreshTimer: freezed == autoRefreshTimer ? _self.autoRefreshTimer : autoRefreshTimer // ignore: cast_nullable_to_non_nullable\nas Timer?,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/provider/server/all.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'all.dart';\n\n// **************************************************************************\n// RiverpodGenerator\n// **************************************************************************\n\n// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, type=warning\n\n@ProviderFor(ServersNotifier)\nconst serversProvider = ServersNotifierProvider._();\n\nfinal class ServersNotifierProvider\n    extends $NotifierProvider<ServersNotifier, ServersState> {\n  const ServersNotifierProvider._()\n    : super(\n        from: null,\n        argument: null,\n        retry: null,\n        name: r'serversProvider',\n        isAutoDispose: false,\n        dependencies: null,\n        $allTransitiveDependencies: null,\n      );\n\n  @override\n  String debugGetCreateSourceHash() => _$serversNotifierHash();\n\n  @$internal\n  @override\n  ServersNotifier create() => ServersNotifier();\n\n  /// {@macro riverpod.override_with_value}\n  Override overrideWithValue(ServersState value) {\n    return $ProviderOverride(\n      origin: this,\n      providerOverride: $SyncValueProvider<ServersState>(value),\n    );\n  }\n}\n\nString _$serversNotifierHash() => r'dc5da44f9bd8d8dcfba3e6e932cca3e2f379e582';\n\nabstract class _$ServersNotifier extends $Notifier<ServersState> {\n  ServersState build();\n  @$mustCallSuper\n  @override\n  void runBuild() {\n    final created = build();\n    final ref = this.ref as $Ref<ServersState, ServersState>;\n    final element =\n        ref.element\n            as $ClassProviderElement<\n              AnyNotifier<ServersState, ServersState>,\n              ServersState,\n              Object?,\n              Object?\n            >;\n    element.handleValue(ref, created);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/server/single.dart",
    "content": "import 'dart:async';\n\nimport 'package:computer/computer.dart';\nimport 'package:dartssh2/dartssh2.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\nimport 'package:server_box/core/extension/ssh_client.dart';\nimport 'package:server_box/core/utils/server.dart';\nimport 'package:server_box/core/utils/ssh_auth.dart';\nimport 'package:server_box/data/helper/ssh_decoder.dart';\nimport 'package:server_box/data/helper/system_detector.dart';\nimport 'package:server_box/data/model/app/error.dart';\nimport 'package:server_box/data/model/app/scripts/script_consts.dart';\nimport 'package:server_box/data/model/app/scripts/shell_func.dart';\nimport 'package:server_box/data/model/server/server.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/server_status_update_req.dart';\nimport 'package:server_box/data/model/server/system.dart';\nimport 'package:server_box/data/model/server/try_limiter.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/res/status.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/data/ssh/session_manager.dart';\n\npart 'single.g.dart';\npart 'single.freezed.dart';\n\n// Individual server state, including connection and status information\n@freezed\nabstract class ServerState with _$ServerState {\n  const factory ServerState({\n    required Spi spi,\n    required ServerStatus status,\n    @Default(ServerConn.disconnected) ServerConn conn,\n    SSHClient? client,\n  }) = _ServerState;\n}\n\n// Individual server state management\n@Riverpod(keepAlive: true)\nclass ServerNotifier extends _$ServerNotifier {\n  @override\n  ServerState build(String serverId) {\n    final serverNotifier = ref.read(serversProvider);\n    final spi = serverNotifier.servers[serverId];\n    if (spi == null) {\n      throw StateError('Server $serverId not found');\n    }\n\n    return ServerState(spi: spi, status: InitStatus.status);\n  }\n\n  // Update connection status\n  void updateConnection(ServerConn conn) {\n    state = state.copyWith(conn: conn);\n  }\n\n  // Update server status\n  void updateStatus(ServerStatus status) {\n    state = state.copyWith(status: status);\n  }\n\n  // Update SSH client\n  void updateClient(SSHClient? client) {\n    state = state.copyWith(client: client);\n  }\n\n  // Update SPI configuration\n  void updateSpi(Spi spi) {\n    state = state.copyWith(spi: spi);\n  }\n\n  // Close connection\n  void closeConnection() {\n    final client = state.client;\n    client?.close();\n    state = state.copyWith(client: null, conn: ServerConn.disconnected);\n  }\n\n  // Refresh server status\n  bool _isRefreshing = false;\n\n  Future<void> refresh() async {\n    if (_isRefreshing) return;\n\n    _isRefreshing = true;\n    try {\n      await _updateServer();\n    } finally {\n      _isRefreshing = false;\n    }\n  }\n\n  Future<void> _updateServer() async {\n    await _getData();\n  }\n\n  Future<void> _getData() async {\n    final spi = state.spi;\n    final sid = spi.id;\n\n    if (!TryLimiter.canTry(sid)) {\n      if (state.conn != ServerConn.failed) {\n        updateConnection(ServerConn.failed);\n      }\n      return;\n    }\n\n    final newStatus = state.status..err = null; // Clear previous error\n    updateStatus(newStatus);\n\n    if (state.conn < ServerConn.connecting || (state.client?.isClosed ?? true)) {\n      updateConnection(ServerConn.connecting);\n\n      // Wake on LAN\n      final wol = spi.wolCfg;\n      if (wol != null) {\n        try {\n          await wol.wake();\n        } catch (e) {\n          Loggers.app.warning('Wake on lan failed', e);\n        }\n      }\n\n      try {\n        final time1 = DateTime.now();\n        final client = await genClient(\n          spi,\n          timeout: Duration(seconds: Stores.setting.timeout.fetch()),\n          onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi),\n        );\n        updateClient(client);\n\n        final time2 = DateTime.now();\n        final spentTime = time2.difference(time1).inMilliseconds;\n        if (spi.jumpId == null) {\n          Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');\n        } else {\n          Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');\n        }\n\n        final sessionId = 'ssh_${spi.id}';\n        TermSessionManager.add(\n          id: sessionId,\n          spi: spi,\n          startTimeMs: time1.millisecondsSinceEpoch,\n          disconnect: () => ref.read(serversProvider.notifier).closeOneServer(spi.id),\n          status: TermSessionStatus.connecting,\n        );\n        TermSessionManager.setActive(sessionId, hasTerminal: false);\n      } catch (e) {\n        TryLimiter.inc(sid);\n\n        final newStatus = state.status..err = SSHErr(type: SSHErrType.connect, message: e.toString());\n        updateStatus(newStatus);\n        updateConnection(ServerConn.failed);\n\n        // Remove SSH session when connection fails\n        final sessionId = 'ssh_${spi.id}';\n        TermSessionManager.remove(sessionId);\n\n        Loggers.app.warning('Connect to ${spi.name} failed', e);\n        return;\n      }\n\n      updateConnection(ServerConn.connected);\n\n      // Update SSH session status to connected\n      final sessionId = 'ssh_${spi.id}';\n      TermSessionManager.updateStatus(sessionId, TermSessionStatus.connected);\n\n      try {\n        // Detect system type\n        final detectedSystemType = await SystemDetector.detect(state.client!, spi);\n        final newStatus = state.status..system = detectedSystemType;\n        updateStatus(newStatus);\n\n        Loggers.app.info('Writing script for ${spi.name} (${detectedSystemType.name})');\n        \n        final (stdoutResult, writeScriptResult) = await state.client!.execSafe(\n          (session) async {\n            final scriptRaw = ShellFuncManager.allScript(\n              spi.custom?.cmds,\n              systemType: detectedSystemType,\n              disabledCmdTypes: spi.disabledCmdTypes,\n            ).uint8List;\n            session.stdin.add(scriptRaw);\n            session.stdin.close();\n          },\n          entry: ShellFuncManager.getInstallShellCmd(\n            spi.id,\n            systemType: detectedSystemType,\n            customDir: spi.custom?.scriptDir,\n          ),\n          systemType: detectedSystemType,\n          context: 'WriteScript<${spi.name}>',\n        );\n        \n        if (stdoutResult.isNotEmpty) {\n          Loggers.app.info('Script write stdout for ${spi.name}: $stdoutResult');\n        }\n        \n        if (writeScriptResult.isNotEmpty) {\n          Loggers.app.warning('Script write stderr for ${spi.name}: $writeScriptResult');\n          if (detectedSystemType != SystemType.windows) {\n            ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);\n            throw writeScriptResult;\n          }\n        } else {\n          Loggers.app.info('Script written successfully for ${spi.name}');\n        }\n      } on SSHAuthAbortError catch (e) {\n        TryLimiter.inc(sid);\n        final err = SSHErr(type: SSHErrType.auth, message: e.toString());\n        final newStatus = state.status..err = err;\n        updateStatus(newStatus);\n        Loggers.app.warning(err);\n        updateConnection(ServerConn.failed);\n\n        final sessionId = 'ssh_${spi.id}';\n        TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);\n        return;\n      } on SSHAuthFailError catch (e) {\n        TryLimiter.inc(sid);\n        final err = SSHErr(type: SSHErrType.auth, message: e.toString());\n        final newStatus = state.status..err = err;\n        updateStatus(newStatus);\n        Loggers.app.warning(err);\n        updateConnection(ServerConn.failed);\n\n        final sessionId = 'ssh_${spi.id}';\n        TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);\n        return;\n      } catch (e) {\n        final err = SSHErr(type: SSHErrType.writeScript, message: e.toString());\n        final newStatus = state.status..err = err;\n        updateStatus(newStatus);\n        Loggers.app.warning(err);\n        updateConnection(ServerConn.failed);\n\n        final sessionId = 'ssh_${spi.id}';\n        TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);\n      }\n    }\n\n    if (state.conn == ServerConn.connecting) return;\n\n    // Keep finished status to prevent UI from refreshing to loading state\n    if (state.conn != ServerConn.finished) {\n      updateConnection(ServerConn.loading);\n    }\n\n    List<String>? segments;\n    String? raw;\n\n    try {\n      final statusCmd = ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir);\n      // Loggers.app.info('Running status command for ${spi.name} (${state.status.system.name}): $statusCmd');\n      final execResult = await state.client?.run(statusCmd);\n      if (execResult != null) {\n        raw = SSHDecoder.decode(\n          execResult,\n          isWindows: state.status.system == SystemType.windows,\n          context: 'GetStatus<${spi.name}>',\n        );\n        // Loggers.app.info('Status response length for ${spi.name}: ${raw.length} bytes');\n      } else {\n        raw = '';\n        Loggers.app.warning('No status result from ${spi.name}');\n      }\n\n      if (raw.isEmpty) {\n        TryLimiter.inc(sid);\n        final newStatus = state.status\n          ..err = SSHErr(type: SSHErrType.segements, message: 'Empty response from server');\n        updateStatus(newStatus);\n        updateConnection(ServerConn.failed);\n\n        final sessionId = 'ssh_${spi.id}';\n        TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);\n        return;\n      }\n\n      segments = raw.split(ScriptConstants.separator).map((e) => e.trim()).toList();\n      if (segments.isEmpty) {\n        if (Stores.setting.keepStatusWhenErr.fetch()) {\n          // Keep previous server status when error occurs\n          if (state.conn != ServerConn.failed && state.status.more.isNotEmpty) {\n            return;\n          }\n        }\n        TryLimiter.inc(sid);\n        final newStatus = state.status\n          ..err = SSHErr(type: SSHErrType.segements, message: 'Separate segments failed, raw:\\n$raw');\n        updateStatus(newStatus);\n        updateConnection(ServerConn.failed);\n\n        final sessionId = 'ssh_${spi.id}';\n        TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);\n        return;\n      }\n    } catch (e) {\n      TryLimiter.inc(sid);\n      final newStatus = state.status..err = SSHErr(type: SSHErrType.getStatus, message: e.toString());\n      updateStatus(newStatus);\n      updateConnection(ServerConn.failed);\n      Loggers.app.warning('Get status from ${spi.name} failed', e);\n\n      final sessionId = 'ssh_${spi.id}';\n      TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);\n      return;\n    }\n\n    try {\n      // Parse script output into command-specific mappings\n      final parsedOutput = ScriptConstants.parseScriptOutput(raw);\n\n      final req = ServerStatusUpdateReq(\n        ss: state.status,\n        parsedOutput: parsedOutput,\n        system: state.status.system,\n        customCmds: spi.custom?.cmds ?? {},\n      );\n      final newStatus = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${spi.id}>');\n      updateStatus(newStatus);\n    } catch (e, trace) {\n      TryLimiter.inc(sid);\n      final newStatus = state.status\n        ..err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\\n\\n$raw');\n      updateStatus(newStatus);\n      updateConnection(ServerConn.failed);\n      Loggers.app.warning('Server status', e, trace);\n\n      final sessionId = 'ssh_${spi.id}';\n      TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);\n      return;\n    }\n\n    // Set Server.isBusy to false each time this method is called\n    updateConnection(ServerConn.finished);\n    // Reset retry count only after successful preparation\n    TryLimiter.reset(sid);\n  }\n}\n\nextension IndividualServerStateExtension on ServerState {\n  bool get needGenClient => conn < ServerConn.connecting;\n\n  bool get canViewDetails => conn == ServerConn.finished;\n\n  String get id => spi.id;\n}\n"
  },
  {
    "path": "lib/data/provider/server/single.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'single.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n/// @nodoc\nmixin _$ServerState {\n\n Spi get spi; ServerStatus get status; ServerConn get conn; SSHClient? get client;\n/// Create a copy of ServerState\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$ServerStateCopyWith<ServerState> get copyWith => _$ServerStateCopyWithImpl<ServerState>(this as ServerState, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,spi,status,conn,client);\n\n@override\nString toString() {\n  return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $ServerStateCopyWith<$Res>  {\n  factory $ServerStateCopyWith(ServerState value, $Res Function(ServerState) _then) = _$ServerStateCopyWithImpl;\n@useResult\n$Res call({\n Spi spi, ServerStatus status, ServerConn conn, SSHClient? client\n});\n\n\n$SpiCopyWith<$Res> get spi;\n\n}\n/// @nodoc\nclass _$ServerStateCopyWithImpl<$Res>\n    implements $ServerStateCopyWith<$Res> {\n  _$ServerStateCopyWithImpl(this._self, this._then);\n\n  final ServerState _self;\n  final $Res Function(ServerState) _then;\n\n/// Create a copy of ServerState\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,}) {\n  return _then(_self.copyWith(\nspi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable\nas Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable\nas ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable\nas ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable\nas SSHClient?,\n  ));\n}\n/// Create a copy of ServerState\n/// with the given fields replaced by the non-null parameter values.\n@override\n@pragma('vm:prefer-inline')\n$SpiCopyWith<$Res> get spi {\n  \n  return $SpiCopyWith<$Res>(_self.spi, (value) {\n    return _then(_self.copyWith(spi: value));\n  });\n}\n}\n\n\n/// Adds pattern-matching-related methods to [ServerState].\nextension ServerStatePatterns on ServerState {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServerState value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _ServerState() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServerState value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _ServerState():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServerState value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _ServerState() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Spi spi,  ServerStatus status,  ServerConn conn,  SSHClient? client)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _ServerState() when $default != null:\nreturn $default(_that.spi,_that.status,_that.conn,_that.client);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Spi spi,  ServerStatus status,  ServerConn conn,  SSHClient? client)  $default,) {final _that = this;\nswitch (_that) {\ncase _ServerState():\nreturn $default(_that.spi,_that.status,_that.conn,_that.client);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Spi spi,  ServerStatus status,  ServerConn conn,  SSHClient? client)?  $default,) {final _that = this;\nswitch (_that) {\ncase _ServerState() when $default != null:\nreturn $default(_that.spi,_that.status,_that.conn,_that.client);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n\nclass _ServerState implements ServerState {\n  const _ServerState({required this.spi, required this.status, this.conn = ServerConn.disconnected, this.client});\n  \n\n@override final  Spi spi;\n@override final  ServerStatus status;\n@override@JsonKey() final  ServerConn conn;\n@override final  SSHClient? client;\n\n/// Create a copy of ServerState\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$ServerStateCopyWith<_ServerState> get copyWith => __$ServerStateCopyWithImpl<_ServerState>(this, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,spi,status,conn,client);\n\n@override\nString toString() {\n  return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$ServerStateCopyWith<$Res> implements $ServerStateCopyWith<$Res> {\n  factory _$ServerStateCopyWith(_ServerState value, $Res Function(_ServerState) _then) = __$ServerStateCopyWithImpl;\n@override @useResult\n$Res call({\n Spi spi, ServerStatus status, ServerConn conn, SSHClient? client\n});\n\n\n@override $SpiCopyWith<$Res> get spi;\n\n}\n/// @nodoc\nclass __$ServerStateCopyWithImpl<$Res>\n    implements _$ServerStateCopyWith<$Res> {\n  __$ServerStateCopyWithImpl(this._self, this._then);\n\n  final _ServerState _self;\n  final $Res Function(_ServerState) _then;\n\n/// Create a copy of ServerState\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,}) {\n  return _then(_ServerState(\nspi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable\nas Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable\nas ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable\nas ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable\nas SSHClient?,\n  ));\n}\n\n/// Create a copy of ServerState\n/// with the given fields replaced by the non-null parameter values.\n@override\n@pragma('vm:prefer-inline')\n$SpiCopyWith<$Res> get spi {\n  \n  return $SpiCopyWith<$Res>(_self.spi, (value) {\n    return _then(_self.copyWith(spi: value));\n  });\n}\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/provider/server/single.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'single.dart';\n\n// **************************************************************************\n// RiverpodGenerator\n// **************************************************************************\n\n// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, type=warning\n\n@ProviderFor(ServerNotifier)\nconst serverProvider = ServerNotifierFamily._();\n\nfinal class ServerNotifierProvider\n    extends $NotifierProvider<ServerNotifier, ServerState> {\n  const ServerNotifierProvider._({\n    required ServerNotifierFamily super.from,\n    required String super.argument,\n  }) : super(\n         retry: null,\n         name: r'serverProvider',\n         isAutoDispose: false,\n         dependencies: null,\n         $allTransitiveDependencies: null,\n       );\n\n  @override\n  String debugGetCreateSourceHash() => _$serverNotifierHash();\n\n  @override\n  String toString() {\n    return r'serverProvider'\n        ''\n        '($argument)';\n  }\n\n  @$internal\n  @override\n  ServerNotifier create() => ServerNotifier();\n\n  /// {@macro riverpod.override_with_value}\n  Override overrideWithValue(ServerState value) {\n    return $ProviderOverride(\n      origin: this,\n      providerOverride: $SyncValueProvider<ServerState>(value),\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    return other is ServerNotifierProvider && other.argument == argument;\n  }\n\n  @override\n  int get hashCode {\n    return argument.hashCode;\n  }\n}\n\nString _$serverNotifierHash() => r'04b1beef4d96242fd10d5b523c6f5f17eb774bae';\n\nfinal class ServerNotifierFamily extends $Family\n    with\n        $ClassFamilyOverride<\n          ServerNotifier,\n          ServerState,\n          ServerState,\n          ServerState,\n          String\n        > {\n  const ServerNotifierFamily._()\n    : super(\n        retry: null,\n        name: r'serverProvider',\n        dependencies: null,\n        $allTransitiveDependencies: null,\n        isAutoDispose: false,\n      );\n\n  ServerNotifierProvider call(String serverId) =>\n      ServerNotifierProvider._(argument: serverId, from: this);\n\n  @override\n  String toString() => r'serverProvider';\n}\n\nabstract class _$ServerNotifier extends $Notifier<ServerState> {\n  late final _$args = ref.$arg as String;\n  String get serverId => _$args;\n\n  ServerState build(String serverId);\n  @$mustCallSuper\n  @override\n  void runBuild() {\n    final created = build(_$args);\n    final ref = this.ref as $Ref<ServerState, ServerState>;\n    final element =\n        ref.element\n            as $ClassProviderElement<\n              AnyNotifier<ServerState, ServerState>,\n              ServerState,\n              Object?,\n              Object?\n            >;\n    element.handleValue(ref, created);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/sftp.dart",
    "content": "import 'dart:async';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\nimport 'package:server_box/data/model/sftp/worker.dart';\n\npart 'sftp.freezed.dart';\npart 'sftp.g.dart';\n\n@freezed\nabstract class SftpState with _$SftpState {\n  const factory SftpState({\n    @Default(<SftpReqStatus>[]) List<SftpReqStatus> requests,\n  }) = _SftpState;\n}\n\n@Riverpod(keepAlive: true)\nclass SftpNotifier extends _$SftpNotifier {\n  @override\n  SftpState build() {\n    return const SftpState();\n  }\n\n  SftpReqStatus? get(int id) {\n    try {\n      return state.requests.singleWhere((element) => element.id == id);\n    } catch (e) {\n      return null;\n    }\n  }\n\n  int add(SftpReq req, {Completer? completer}) {\n    final reqStat = SftpReqStatus(\n      notifyListeners: _notifyListeners,\n      completer: completer,\n      req: req,\n    );\n    state = state.copyWith(\n      requests: [...state.requests, reqStat],\n    );\n    return reqStat.id;\n  }\n\n  void dispose() {\n    for (final item in state.requests) {\n      item.dispose();\n    }\n    state = state.copyWith(requests: []);\n  }\n\n  void cancel(int id) {\n    final idx = state.requests.indexWhere((e) => e.id == id);\n    if (idx < 0 || idx >= state.requests.length) {\n      dprint('SftpProvider.cancel: id $id not found');\n      return;\n    }\n    final item = state.requests[idx];\n    item.dispose();\n    final newRequests = List<SftpReqStatus>.from(state.requests)\n      ..removeAt(idx);\n    state = state.copyWith(requests: newRequests);\n  }\n\n  void _notifyListeners() {\n    // Force state update to notify listeners\n    state = state.copyWith();\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/sftp.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'sftp.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n/// @nodoc\nmixin _$SftpState {\n\n List<SftpReqStatus> get requests;\n/// Create a copy of SftpState\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$SftpStateCopyWith<SftpState> get copyWith => _$SftpStateCopyWithImpl<SftpState>(this as SftpState, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is SftpState&&const DeepCollectionEquality().equals(other.requests, requests));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(requests));\n\n@override\nString toString() {\n  return 'SftpState(requests: $requests)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $SftpStateCopyWith<$Res>  {\n  factory $SftpStateCopyWith(SftpState value, $Res Function(SftpState) _then) = _$SftpStateCopyWithImpl;\n@useResult\n$Res call({\n List<SftpReqStatus> requests\n});\n\n\n\n\n}\n/// @nodoc\nclass _$SftpStateCopyWithImpl<$Res>\n    implements $SftpStateCopyWith<$Res> {\n  _$SftpStateCopyWithImpl(this._self, this._then);\n\n  final SftpState _self;\n  final $Res Function(SftpState) _then;\n\n/// Create a copy of SftpState\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? requests = null,}) {\n  return _then(_self.copyWith(\nrequests: null == requests ? _self.requests : requests // ignore: cast_nullable_to_non_nullable\nas List<SftpReqStatus>,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [SftpState].\nextension SftpStatePatterns on SftpState {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SftpState value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _SftpState() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SftpState value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SftpState():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SftpState value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SftpState() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<SftpReqStatus> requests)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _SftpState() when $default != null:\nreturn $default(_that.requests);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<SftpReqStatus> requests)  $default,) {final _that = this;\nswitch (_that) {\ncase _SftpState():\nreturn $default(_that.requests);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<SftpReqStatus> requests)?  $default,) {final _that = this;\nswitch (_that) {\ncase _SftpState() when $default != null:\nreturn $default(_that.requests);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n\nclass _SftpState implements SftpState {\n  const _SftpState({final  List<SftpReqStatus> requests = const <SftpReqStatus>[]}): _requests = requests;\n  \n\n final  List<SftpReqStatus> _requests;\n@override@JsonKey() List<SftpReqStatus> get requests {\n  if (_requests is EqualUnmodifiableListView) return _requests;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(_requests);\n}\n\n\n/// Create a copy of SftpState\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$SftpStateCopyWith<_SftpState> get copyWith => __$SftpStateCopyWithImpl<_SftpState>(this, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SftpState&&const DeepCollectionEquality().equals(other._requests, _requests));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_requests));\n\n@override\nString toString() {\n  return 'SftpState(requests: $requests)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$SftpStateCopyWith<$Res> implements $SftpStateCopyWith<$Res> {\n  factory _$SftpStateCopyWith(_SftpState value, $Res Function(_SftpState) _then) = __$SftpStateCopyWithImpl;\n@override @useResult\n$Res call({\n List<SftpReqStatus> requests\n});\n\n\n\n\n}\n/// @nodoc\nclass __$SftpStateCopyWithImpl<$Res>\n    implements _$SftpStateCopyWith<$Res> {\n  __$SftpStateCopyWithImpl(this._self, this._then);\n\n  final _SftpState _self;\n  final $Res Function(_SftpState) _then;\n\n/// Create a copy of SftpState\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? requests = null,}) {\n  return _then(_SftpState(\nrequests: null == requests ? _self._requests : requests // ignore: cast_nullable_to_non_nullable\nas List<SftpReqStatus>,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/provider/sftp.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'sftp.dart';\n\n// **************************************************************************\n// RiverpodGenerator\n// **************************************************************************\n\n// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, type=warning\n\n@ProviderFor(SftpNotifier)\nconst sftpProvider = SftpNotifierProvider._();\n\nfinal class SftpNotifierProvider\n    extends $NotifierProvider<SftpNotifier, SftpState> {\n  const SftpNotifierProvider._()\n    : super(\n        from: null,\n        argument: null,\n        retry: null,\n        name: r'sftpProvider',\n        isAutoDispose: false,\n        dependencies: null,\n        $allTransitiveDependencies: null,\n      );\n\n  @override\n  String debugGetCreateSourceHash() => _$sftpNotifierHash();\n\n  @$internal\n  @override\n  SftpNotifier create() => SftpNotifier();\n\n  /// {@macro riverpod.override_with_value}\n  Override overrideWithValue(SftpState value) {\n    return $ProviderOverride(\n      origin: this,\n      providerOverride: $SyncValueProvider<SftpState>(value),\n    );\n  }\n}\n\nString _$sftpNotifierHash() => r'f8412a4bd1f2bc5919ec31a3eba1c27e9a578f41';\n\nabstract class _$SftpNotifier extends $Notifier<SftpState> {\n  SftpState build();\n  @$mustCallSuper\n  @override\n  void runBuild() {\n    final created = build();\n    final ref = this.ref as $Ref<SftpState, SftpState>;\n    final element =\n        ref.element\n            as $ClassProviderElement<\n              AnyNotifier<SftpState, SftpState>,\n              SftpState,\n              Object?,\n              Object?\n            >;\n    element.handleValue(ref, created);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/snippet.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\nimport 'package:server_box/core/sync.dart';\nimport 'package:server_box/data/model/server/snippet.dart';\nimport 'package:server_box/data/res/store.dart';\n\npart 'snippet.freezed.dart';\npart 'snippet.g.dart';\n\n@freezed\nabstract class SnippetState with _$SnippetState {\n  const factory SnippetState({\n    @Default(<Snippet>[]) List<Snippet> snippets,\n    @Default(<String>{}) Set<String> tags,\n  }) = _SnippetState;\n}\n\n@Riverpod(keepAlive: true)\nclass SnippetNotifier extends _$SnippetNotifier {\n  @override\n  SnippetState build() {\n    return _load();\n  }\n\n  void reload() {\n    final newState = _load();\n    if (newState == state) return;\n    state = newState;\n  }\n\n  SnippetState _load() {\n    final snippets = Stores.snippet.fetch();\n    final order = Stores.setting.snippetOrder.fetch();\n\n    List<Snippet> orderedSnippets = snippets;\n    if (order.isNotEmpty) {\n      final surplus = snippets.reorder(order: order, finder: (n, name) => n.name == name);\n      order.removeWhere((e) => surplus.any((ele) => ele == e));\n      if (order != Stores.setting.snippetOrder.fetch()) {\n        Stores.setting.snippetOrder.put(order);\n      }\n      orderedSnippets = snippets;\n    }\n\n    final newTags = _computeTags(orderedSnippets);\n    return stateOrNull?.copyWith(snippets: orderedSnippets, tags: newTags) ??\n        SnippetState(snippets: orderedSnippets, tags: newTags);\n  }\n\n  Set<String> _computeTags(List<Snippet> snippets) {\n    final tags = <String>{};\n    for (final s in snippets) {\n      final t = s.tags;\n      if (t != null) {\n        tags.addAll(t);\n      }\n    }\n    return tags;\n  }\n\n  void add(Snippet snippet) {\n    final newSnippets = [...state.snippets, snippet];\n    final newTags = _computeTags(newSnippets);\n    state = state.copyWith(snippets: newSnippets, tags: newTags);\n    Stores.snippet.put(snippet);\n    bakSync.sync(milliDelay: 1000);\n  }\n\n  void del(Snippet snippet) {\n    final newSnippets = state.snippets.where((s) => s != snippet).toList();\n    final newTags = _computeTags(newSnippets);\n    state = state.copyWith(snippets: newSnippets, tags: newTags);\n    Stores.snippet.delete(snippet);\n    bakSync.sync(milliDelay: 1000);\n  }\n\n  void update(Snippet old, Snippet newOne) {\n    final newSnippets = state.snippets.map((s) => s == old ? newOne : s).toList();\n    final newTags = _computeTags(newSnippets);\n    state = state.copyWith(snippets: newSnippets, tags: newTags);\n    Stores.snippet.delete(old);\n    Stores.snippet.put(newOne);\n    bakSync.sync(milliDelay: 1000);\n  }\n\n  void renameTag(String old, String newOne) {\n    final updatedSnippets = <Snippet>[];\n    for (final s in state.snippets) {\n      if (s.tags?.contains(old) ?? false) {\n        final newTags = Set<String>.from(s.tags!);\n        newTags.remove(old);\n        newTags.add(newOne);\n        final updatedSnippet = s.copyWith(tags: newTags.toList());\n        updatedSnippets.add(updatedSnippet);\n        Stores.snippet.put(updatedSnippet);\n      } else {\n        updatedSnippets.add(s);\n      }\n    }\n    final newTags = _computeTags(updatedSnippets);\n    state = state.copyWith(snippets: updatedSnippets, tags: newTags);\n    bakSync.sync(milliDelay: 1000);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/snippet.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'snippet.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n/// @nodoc\nmixin _$SnippetState {\n\n List<Snippet> get snippets; Set<String> get tags;\n/// Create a copy of SnippetState\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$SnippetStateCopyWith<SnippetState> get copyWith => _$SnippetStateCopyWithImpl<SnippetState>(this as SnippetState, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is SnippetState&&const DeepCollectionEquality().equals(other.snippets, snippets)&&const DeepCollectionEquality().equals(other.tags, tags));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(snippets),const DeepCollectionEquality().hash(tags));\n\n@override\nString toString() {\n  return 'SnippetState(snippets: $snippets, tags: $tags)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $SnippetStateCopyWith<$Res>  {\n  factory $SnippetStateCopyWith(SnippetState value, $Res Function(SnippetState) _then) = _$SnippetStateCopyWithImpl;\n@useResult\n$Res call({\n List<Snippet> snippets, Set<String> tags\n});\n\n\n\n\n}\n/// @nodoc\nclass _$SnippetStateCopyWithImpl<$Res>\n    implements $SnippetStateCopyWith<$Res> {\n  _$SnippetStateCopyWithImpl(this._self, this._then);\n\n  final SnippetState _self;\n  final $Res Function(SnippetState) _then;\n\n/// Create a copy of SnippetState\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? snippets = null,Object? tags = null,}) {\n  return _then(_self.copyWith(\nsnippets: null == snippets ? _self.snippets : snippets // ignore: cast_nullable_to_non_nullable\nas List<Snippet>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable\nas Set<String>,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [SnippetState].\nextension SnippetStatePatterns on SnippetState {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnippetState value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _SnippetState() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnippetState value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SnippetState():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnippetState value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SnippetState() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<Snippet> snippets,  Set<String> tags)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _SnippetState() when $default != null:\nreturn $default(_that.snippets,_that.tags);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<Snippet> snippets,  Set<String> tags)  $default,) {final _that = this;\nswitch (_that) {\ncase _SnippetState():\nreturn $default(_that.snippets,_that.tags);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<Snippet> snippets,  Set<String> tags)?  $default,) {final _that = this;\nswitch (_that) {\ncase _SnippetState() when $default != null:\nreturn $default(_that.snippets,_that.tags);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n\nclass _SnippetState implements SnippetState {\n  const _SnippetState({final  List<Snippet> snippets = const <Snippet>[], final  Set<String> tags = const <String>{}}): _snippets = snippets,_tags = tags;\n  \n\n final  List<Snippet> _snippets;\n@override@JsonKey() List<Snippet> get snippets {\n  if (_snippets is EqualUnmodifiableListView) return _snippets;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(_snippets);\n}\n\n final  Set<String> _tags;\n@override@JsonKey() Set<String> get tags {\n  if (_tags is EqualUnmodifiableSetView) return _tags;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableSetView(_tags);\n}\n\n\n/// Create a copy of SnippetState\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$SnippetStateCopyWith<_SnippetState> get copyWith => __$SnippetStateCopyWithImpl<_SnippetState>(this, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnippetState&&const DeepCollectionEquality().equals(other._snippets, _snippets)&&const DeepCollectionEquality().equals(other._tags, _tags));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_snippets),const DeepCollectionEquality().hash(_tags));\n\n@override\nString toString() {\n  return 'SnippetState(snippets: $snippets, tags: $tags)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$SnippetStateCopyWith<$Res> implements $SnippetStateCopyWith<$Res> {\n  factory _$SnippetStateCopyWith(_SnippetState value, $Res Function(_SnippetState) _then) = __$SnippetStateCopyWithImpl;\n@override @useResult\n$Res call({\n List<Snippet> snippets, Set<String> tags\n});\n\n\n\n\n}\n/// @nodoc\nclass __$SnippetStateCopyWithImpl<$Res>\n    implements _$SnippetStateCopyWith<$Res> {\n  __$SnippetStateCopyWithImpl(this._self, this._then);\n\n  final _SnippetState _self;\n  final $Res Function(_SnippetState) _then;\n\n/// Create a copy of SnippetState\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? snippets = null,Object? tags = null,}) {\n  return _then(_SnippetState(\nsnippets: null == snippets ? _self._snippets : snippets // ignore: cast_nullable_to_non_nullable\nas List<Snippet>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable\nas Set<String>,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/provider/snippet.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'snippet.dart';\n\n// **************************************************************************\n// RiverpodGenerator\n// **************************************************************************\n\n// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, type=warning\n\n@ProviderFor(SnippetNotifier)\nconst snippetProvider = SnippetNotifierProvider._();\n\nfinal class SnippetNotifierProvider\n    extends $NotifierProvider<SnippetNotifier, SnippetState> {\n  const SnippetNotifierProvider._()\n    : super(\n        from: null,\n        argument: null,\n        retry: null,\n        name: r'snippetProvider',\n        isAutoDispose: false,\n        dependencies: null,\n        $allTransitiveDependencies: null,\n      );\n\n  @override\n  String debugGetCreateSourceHash() => _$snippetNotifierHash();\n\n  @$internal\n  @override\n  SnippetNotifier create() => SnippetNotifier();\n\n  /// {@macro riverpod.override_with_value}\n  Override overrideWithValue(SnippetState value) {\n    return $ProviderOverride(\n      origin: this,\n      providerOverride: $SyncValueProvider<SnippetState>(value),\n    );\n  }\n}\n\nString _$snippetNotifierHash() => r'8285c7edf905a4aaa41cd8b65b0a6755c8b97fc9';\n\nabstract class _$SnippetNotifier extends $Notifier<SnippetState> {\n  SnippetState build();\n  @$mustCallSuper\n  @override\n  void runBuild() {\n    final created = build();\n    final ref = this.ref as $Ref<SnippetState, SnippetState>;\n    final element =\n        ref.element\n            as $ClassProviderElement<\n              AnyNotifier<SnippetState, SnippetState>,\n              SnippetState,\n              Object?,\n              Object?\n            >;\n    element.handleValue(ref, created);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/systemd.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\nimport 'package:server_box/core/extension/ssh_client.dart';\nimport 'package:server_box/data/model/app/scripts/script_consts.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/systemd.dart';\nimport 'package:server_box/data/provider/server/single.dart';\n\npart 'systemd.freezed.dart';\npart 'systemd.g.dart';\n\n@freezed\nabstract class SystemdState with _$SystemdState {\n  const factory SystemdState({\n    @Default(false) bool isBusy,\n    @Default(<SystemdUnit>[]) List<SystemdUnit> units,\n    @Default(SystemdScopeFilter.all) SystemdScopeFilter scopeFilter,\n  }) = _SystemdState;\n}\n\n@riverpod\nclass SystemdNotifier extends _$SystemdNotifier {\n  late final ServerState _si;\n\n  @override\n  SystemdState build(Spi spi) {\n    final si = ref.read(serverProvider(spi.id));\n    _si = si;\n    // Async initialization\n    Future.microtask(() => getUnits());\n    return const SystemdState();\n  }\n\n  List<SystemdUnit> get filteredUnits {\n    switch (state.scopeFilter) {\n      case SystemdScopeFilter.all:\n        return state.units;\n      case SystemdScopeFilter.system:\n        return state.units.where((unit) => unit.scope == SystemdUnitScope.system).toList();\n      case SystemdScopeFilter.user:\n        return state.units.where((unit) => unit.scope == SystemdUnitScope.user).toList();\n    }\n  }\n\n  void setScopeFilter(SystemdScopeFilter filter) {\n    state = state.copyWith(scopeFilter: filter);\n  }\n\n  Future<void> getUnits() async {\n    state = state.copyWith(isBusy: true);\n\n    try {\n      final client = _si.client;\n      final result = await client!.execForOutput(_getUnitsCmd);\n      final units = result.split('\\n');\n\n      final userUnits = <String>[];\n      final systemUnits = <String>[];\n      for (final unit in units) {\n        final maybeSystem = unit.contains('/systemd/system');\n        final maybeUser = unit.contains('/.config/systemd/user');\n        if (maybeSystem && !maybeUser) {\n          systemUnits.add(unit);\n        } else {\n          userUnits.add(unit);\n        }\n      }\n\n      final parsedUserUnits = await _parseUnitObj(userUnits, SystemdUnitScope.user);\n      final parsedSystemUnits = await _parseUnitObj(systemUnits, SystemdUnitScope.system);\n      state = state.copyWith(units: [...parsedUserUnits, ...parsedSystemUnits], isBusy: false);\n    } catch (e, s) {\n      dprint('Parse systemd', e, s);\n      state = state.copyWith(isBusy: false);\n    }\n  }\n\n  Future<List<SystemdUnit>> _parseUnitObj(List<String> unitNames, SystemdUnitScope scope) async {\n    final unitNames_ = unitNames.map((e) {\n      final fullName = e.trim().split('/').last;\n      final lastDot = fullName.lastIndexOf('.');\n      final name = lastDot > 0 ? fullName.substring(0, lastDot) : fullName;\n      return name.replaceAll(RegExp(r'[^a-zA-Z0-9\\-_.@:]'), '');\n    }).toList();\n    final script =\n        '''\nfor unit in ${unitNames_.map((e) => '\"$e\"').join(' ')}; do\n  state=\\$(systemctl show --no-pager -- \"\\$unit\")\n  echo \"\\$state\"\n  echo -n \"\\n${ScriptConstants.separator}\\n\"\ndone\n''';\n    final client = _si.client!;\n    final result = await client.execForOutput(script);\n    final units = result.split(ScriptConstants.separator);\n\n    final parsedUnits = <SystemdUnit>[];\n    for (final unit in units.where((e) => e.trim().isNotEmpty)) {\n      final parts = unit.split('\\n').where((e) => e.trim().isNotEmpty).toList();\n      if (parts.isEmpty) continue;\n      var name = '';\n      var type = '';\n      var state = '';\n      String? description;\n      for (final part in parts) {\n        if (part.startsWith('Id=')) {\n          final val = _getIniVal(part);\n          if (val == null) continue;\n          // Id=org.cups.cupsd.service\n          final lastDot = val.lastIndexOf('.');\n          if (lastDot > 0) {\n            name = val.substring(0, lastDot);\n            type = val.substring(lastDot + 1);\n          } else {\n            name = val;\n            type = '';\n          }\n          continue;\n        }\n        if (part.startsWith('ActiveState=')) {\n          final val = _getIniVal(part);\n          if (val == null) continue;\n          state = val;\n          continue;\n        }\n        if (part.startsWith('Description=')) {\n          description = _getIniVal(part);\n          continue;\n        }\n      }\n\n      final unitType = SystemdUnitType.fromString(type);\n      if (unitType == null) {\n        dprint('Unit type: $type');\n        continue;\n      }\n      final unitState = SystemdUnitState.fromString(state);\n      if (unitState == null) {\n        dprint('Unit state: $state');\n        continue;\n      }\n\n      parsedUnits.add(\n        SystemdUnit(name: name, type: unitType, scope: scope, state: unitState, description: description),\n      );\n    }\n\n    parsedUnits.sort((a, b) {\n      // user units first\n      if (a.scope != b.scope) {\n        return a.scope == SystemdUnitScope.user ? -1 : 1;\n      }\n      // active units first\n      if (a.state != b.state) {\n        return a.state == SystemdUnitState.active ? -1 : 1;\n      }\n      return a.name.compareTo(b.name);\n    });\n    return parsedUnits;\n  }\n\n  late final _getUnitsCmd = '''\n    types=\"service socket mount timer\"\n\n    get_files() {\n      unit_type=\\$1\n      base_dir=\\$2\n      [ -d \"\\$base_dir\" ] || return\n      find \"\\$base_dir\" -type f -name \"*.\\$unit_type\" -print\n    }\n\n    for type in \\$types; do\n      get_files \\$type /etc/systemd/system\n      # Parsing these paths can lead to SSH transport closed errors\n      # get_files \\$type /lib/systemd/system\n      # get_files \\$type /usr/lib/systemd/system\n      get_files \\$type ~/.config/systemd/user\n    done | sort\n    ''';\n}\n\nString? _getIniVal(String line) {\n  final idx = line.indexOf('=');\n  if (idx < 0) return null;\n  return line.substring(idx + 1).trim();\n}\n"
  },
  {
    "path": "lib/data/provider/systemd.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'systemd.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n/// @nodoc\nmixin _$SystemdState {\n\n bool get isBusy; List<SystemdUnit> get units; SystemdScopeFilter get scopeFilter;\n/// Create a copy of SystemdState\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$SystemdStateCopyWith<SystemdState> get copyWith => _$SystemdStateCopyWithImpl<SystemdState>(this as SystemdState, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is SystemdState&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&const DeepCollectionEquality().equals(other.units, units)&&(identical(other.scopeFilter, scopeFilter) || other.scopeFilter == scopeFilter));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,isBusy,const DeepCollectionEquality().hash(units),scopeFilter);\n\n@override\nString toString() {\n  return 'SystemdState(isBusy: $isBusy, units: $units, scopeFilter: $scopeFilter)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $SystemdStateCopyWith<$Res>  {\n  factory $SystemdStateCopyWith(SystemdState value, $Res Function(SystemdState) _then) = _$SystemdStateCopyWithImpl;\n@useResult\n$Res call({\n bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter\n});\n\n\n\n\n}\n/// @nodoc\nclass _$SystemdStateCopyWithImpl<$Res>\n    implements $SystemdStateCopyWith<$Res> {\n  _$SystemdStateCopyWithImpl(this._self, this._then);\n\n  final SystemdState _self;\n  final $Res Function(SystemdState) _then;\n\n/// Create a copy of SystemdState\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? isBusy = null,Object? units = null,Object? scopeFilter = null,}) {\n  return _then(_self.copyWith(\nisBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable\nas bool,units: null == units ? _self.units : units // ignore: cast_nullable_to_non_nullable\nas List<SystemdUnit>,scopeFilter: null == scopeFilter ? _self.scopeFilter : scopeFilter // ignore: cast_nullable_to_non_nullable\nas SystemdScopeFilter,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [SystemdState].\nextension SystemdStatePatterns on SystemdState {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SystemdState value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _SystemdState() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SystemdState value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SystemdState():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SystemdState value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _SystemdState() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isBusy,  List<SystemdUnit> units,  SystemdScopeFilter scopeFilter)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _SystemdState() when $default != null:\nreturn $default(_that.isBusy,_that.units,_that.scopeFilter);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isBusy,  List<SystemdUnit> units,  SystemdScopeFilter scopeFilter)  $default,) {final _that = this;\nswitch (_that) {\ncase _SystemdState():\nreturn $default(_that.isBusy,_that.units,_that.scopeFilter);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isBusy,  List<SystemdUnit> units,  SystemdScopeFilter scopeFilter)?  $default,) {final _that = this;\nswitch (_that) {\ncase _SystemdState() when $default != null:\nreturn $default(_that.isBusy,_that.units,_that.scopeFilter);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n\nclass _SystemdState implements SystemdState {\n  const _SystemdState({this.isBusy = false, final  List<SystemdUnit> units = const <SystemdUnit>[], this.scopeFilter = SystemdScopeFilter.all}): _units = units;\n  \n\n@override@JsonKey() final  bool isBusy;\n final  List<SystemdUnit> _units;\n@override@JsonKey() List<SystemdUnit> get units {\n  if (_units is EqualUnmodifiableListView) return _units;\n  // ignore: implicit_dynamic_type\n  return EqualUnmodifiableListView(_units);\n}\n\n@override@JsonKey() final  SystemdScopeFilter scopeFilter;\n\n/// Create a copy of SystemdState\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$SystemdStateCopyWith<_SystemdState> get copyWith => __$SystemdStateCopyWithImpl<_SystemdState>(this, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SystemdState&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&const DeepCollectionEquality().equals(other._units, _units)&&(identical(other.scopeFilter, scopeFilter) || other.scopeFilter == scopeFilter));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,isBusy,const DeepCollectionEquality().hash(_units),scopeFilter);\n\n@override\nString toString() {\n  return 'SystemdState(isBusy: $isBusy, units: $units, scopeFilter: $scopeFilter)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$SystemdStateCopyWith<$Res> implements $SystemdStateCopyWith<$Res> {\n  factory _$SystemdStateCopyWith(_SystemdState value, $Res Function(_SystemdState) _then) = __$SystemdStateCopyWithImpl;\n@override @useResult\n$Res call({\n bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter\n});\n\n\n\n\n}\n/// @nodoc\nclass __$SystemdStateCopyWithImpl<$Res>\n    implements _$SystemdStateCopyWith<$Res> {\n  __$SystemdStateCopyWithImpl(this._self, this._then);\n\n  final _SystemdState _self;\n  final $Res Function(_SystemdState) _then;\n\n/// Create a copy of SystemdState\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? isBusy = null,Object? units = null,Object? scopeFilter = null,}) {\n  return _then(_SystemdState(\nisBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable\nas bool,units: null == units ? _self._units : units // ignore: cast_nullable_to_non_nullable\nas List<SystemdUnit>,scopeFilter: null == scopeFilter ? _self.scopeFilter : scopeFilter // ignore: cast_nullable_to_non_nullable\nas SystemdScopeFilter,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/provider/systemd.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'systemd.dart';\n\n// **************************************************************************\n// RiverpodGenerator\n// **************************************************************************\n\n// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, type=warning\n\n@ProviderFor(SystemdNotifier)\nconst systemdProvider = SystemdNotifierFamily._();\n\nfinal class SystemdNotifierProvider\n    extends $NotifierProvider<SystemdNotifier, SystemdState> {\n  const SystemdNotifierProvider._({\n    required SystemdNotifierFamily super.from,\n    required Spi super.argument,\n  }) : super(\n         retry: null,\n         name: r'systemdProvider',\n         isAutoDispose: true,\n         dependencies: null,\n         $allTransitiveDependencies: null,\n       );\n\n  @override\n  String debugGetCreateSourceHash() => _$systemdNotifierHash();\n\n  @override\n  String toString() {\n    return r'systemdProvider'\n        ''\n        '($argument)';\n  }\n\n  @$internal\n  @override\n  SystemdNotifier create() => SystemdNotifier();\n\n  /// {@macro riverpod.override_with_value}\n  Override overrideWithValue(SystemdState value) {\n    return $ProviderOverride(\n      origin: this,\n      providerOverride: $SyncValueProvider<SystemdState>(value),\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    return other is SystemdNotifierProvider && other.argument == argument;\n  }\n\n  @override\n  int get hashCode {\n    return argument.hashCode;\n  }\n}\n\nString _$systemdNotifierHash() => r'030d556efc3d897419cd3462d37cb705813e24c7';\n\nfinal class SystemdNotifierFamily extends $Family\n    with\n        $ClassFamilyOverride<\n          SystemdNotifier,\n          SystemdState,\n          SystemdState,\n          SystemdState,\n          Spi\n        > {\n  const SystemdNotifierFamily._()\n    : super(\n        retry: null,\n        name: r'systemdProvider',\n        dependencies: null,\n        $allTransitiveDependencies: null,\n        isAutoDispose: true,\n      );\n\n  SystemdNotifierProvider call(Spi spi) =>\n      SystemdNotifierProvider._(argument: spi, from: this);\n\n  @override\n  String toString() => r'systemdProvider';\n}\n\nabstract class _$SystemdNotifier extends $Notifier<SystemdState> {\n  late final _$args = ref.$arg as Spi;\n  Spi get spi => _$args;\n\n  SystemdState build(Spi spi);\n  @$mustCallSuper\n  @override\n  void runBuild() {\n    final created = build(_$args);\n    final ref = this.ref as $Ref<SystemdState, SystemdState>;\n    final element =\n        ref.element\n            as $ClassProviderElement<\n              AnyNotifier<SystemdState, SystemdState>,\n              SystemdState,\n              Object?,\n              Object?\n            >;\n    element.handleValue(ref, created);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/virtual_keyboard.dart",
    "content": "import 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:xterm/core.dart';\n\npart 'virtual_keyboard.g.dart';\npart 'virtual_keyboard.freezed.dart';\n\n@freezed\nabstract class VirtKeyState with _$VirtKeyState {\n  const factory VirtKeyState({\n    @Default(false) final bool ctrl,\n    @Default(false) final bool alt,\n    @Default(false) final bool shift,\n  }) = _VirtKeyState;\n}\n\n@riverpod\nclass VirtKeyboard extends _$VirtKeyboard implements TerminalInputHandler {\n  @override\n  VirtKeyState build() {\n    return const VirtKeyState();\n  }\n\n  bool get ctrl => state.ctrl;\n  bool get alt => state.alt;\n  bool get shift => state.shift;\n\n  void setCtrl(bool value) {\n    if (value != state.ctrl) {\n      state = state.copyWith(ctrl: value);\n    }\n  }\n\n  void setAlt(bool value) {\n    if (value != state.alt) {\n      state = state.copyWith(alt: value);\n    }\n  }\n\n  void setShift(bool value) {\n    if (value != state.shift) {\n      state = state.copyWith(shift: value);\n    }\n  }\n\n  void reset(TerminalKeyboardEvent e) {\n    state = state.copyWith(\n      ctrl: e.ctrl ? false : state.ctrl,\n      alt: e.alt ? false : state.alt,\n      shift: e.shift ? false : state.shift,\n    );\n  }\n\n  @override\n  String? call(TerminalKeyboardEvent event) {\n    final e = event.copyWith(\n      ctrl: event.ctrl || state.ctrl,\n      alt: event.alt || state.alt,\n      shift: event.shift || state.shift,\n    );\n    if (Stores.setting.sshVirtualKeyAutoOff.fetch()) {\n      reset(e);\n    }\n    return defaultInputHandler.call(e);\n  }\n}\n"
  },
  {
    "path": "lib/data/provider/virtual_keyboard.freezed.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n// coverage:ignore-file\n// ignore_for_file: type=lint\n// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark\n\npart of 'virtual_keyboard.dart';\n\n// **************************************************************************\n// FreezedGenerator\n// **************************************************************************\n\n// dart format off\nT _$identity<T>(T value) => value;\n/// @nodoc\nmixin _$VirtKeyState {\n\n bool get ctrl; bool get alt; bool get shift;\n/// Create a copy of VirtKeyState\n/// with the given fields replaced by the non-null parameter values.\n@JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n$VirtKeyStateCopyWith<VirtKeyState> get copyWith => _$VirtKeyStateCopyWithImpl<VirtKeyState>(this as VirtKeyState, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is VirtKeyState&&(identical(other.ctrl, ctrl) || other.ctrl == ctrl)&&(identical(other.alt, alt) || other.alt == alt)&&(identical(other.shift, shift) || other.shift == shift));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,ctrl,alt,shift);\n\n@override\nString toString() {\n  return 'VirtKeyState(ctrl: $ctrl, alt: $alt, shift: $shift)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class $VirtKeyStateCopyWith<$Res>  {\n  factory $VirtKeyStateCopyWith(VirtKeyState value, $Res Function(VirtKeyState) _then) = _$VirtKeyStateCopyWithImpl;\n@useResult\n$Res call({\n bool ctrl, bool alt, bool shift\n});\n\n\n\n\n}\n/// @nodoc\nclass _$VirtKeyStateCopyWithImpl<$Res>\n    implements $VirtKeyStateCopyWith<$Res> {\n  _$VirtKeyStateCopyWithImpl(this._self, this._then);\n\n  final VirtKeyState _self;\n  final $Res Function(VirtKeyState) _then;\n\n/// Create a copy of VirtKeyState\n/// with the given fields replaced by the non-null parameter values.\n@pragma('vm:prefer-inline') @override $Res call({Object? ctrl = null,Object? alt = null,Object? shift = null,}) {\n  return _then(_self.copyWith(\nctrl: null == ctrl ? _self.ctrl : ctrl // ignore: cast_nullable_to_non_nullable\nas bool,alt: null == alt ? _self.alt : alt // ignore: cast_nullable_to_non_nullable\nas bool,shift: null == shift ? _self.shift : shift // ignore: cast_nullable_to_non_nullable\nas bool,\n  ));\n}\n\n}\n\n\n/// Adds pattern-matching-related methods to [VirtKeyState].\nextension VirtKeyStatePatterns on VirtKeyState {\n/// A variant of `map` that fallback to returning `orElse`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _VirtKeyState value)?  $default,{required TResult orElse(),}){\nfinal _that = this;\nswitch (_that) {\ncase _VirtKeyState() when $default != null:\nreturn $default(_that);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// Callbacks receives the raw object, upcasted.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case final Subclass2 value:\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _VirtKeyState value)  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _VirtKeyState():\nreturn $default(_that);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `map` that fallback to returning `null`.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case final Subclass value:\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _VirtKeyState value)?  $default,){\nfinal _that = this;\nswitch (_that) {\ncase _VirtKeyState() when $default != null:\nreturn $default(_that);case _:\n  return null;\n\n}\n}\n/// A variant of `when` that fallback to an `orElse` callback.\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return orElse();\n/// }\n/// ```\n\n@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool ctrl,  bool alt,  bool shift)?  $default,{required TResult orElse(),}) {final _that = this;\nswitch (_that) {\ncase _VirtKeyState() when $default != null:\nreturn $default(_that.ctrl,_that.alt,_that.shift);case _:\n  return orElse();\n\n}\n}\n/// A `switch`-like method, using callbacks.\n///\n/// As opposed to `map`, this offers destructuring.\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case Subclass2(:final field2):\n///     return ...;\n/// }\n/// ```\n\n@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool ctrl,  bool alt,  bool shift)  $default,) {final _that = this;\nswitch (_that) {\ncase _VirtKeyState():\nreturn $default(_that.ctrl,_that.alt,_that.shift);case _:\n  throw StateError('Unexpected subclass');\n\n}\n}\n/// A variant of `when` that fallback to returning `null`\n///\n/// It is equivalent to doing:\n/// ```dart\n/// switch (sealedClass) {\n///   case Subclass(:final field):\n///     return ...;\n///   case _:\n///     return null;\n/// }\n/// ```\n\n@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool ctrl,  bool alt,  bool shift)?  $default,) {final _that = this;\nswitch (_that) {\ncase _VirtKeyState() when $default != null:\nreturn $default(_that.ctrl,_that.alt,_that.shift);case _:\n  return null;\n\n}\n}\n\n}\n\n/// @nodoc\n\n\nclass _VirtKeyState implements VirtKeyState {\n  const _VirtKeyState({this.ctrl = false, this.alt = false, this.shift = false});\n  \n\n@override@JsonKey() final  bool ctrl;\n@override@JsonKey() final  bool alt;\n@override@JsonKey() final  bool shift;\n\n/// Create a copy of VirtKeyState\n/// with the given fields replaced by the non-null parameter values.\n@override @JsonKey(includeFromJson: false, includeToJson: false)\n@pragma('vm:prefer-inline')\n_$VirtKeyStateCopyWith<_VirtKeyState> get copyWith => __$VirtKeyStateCopyWithImpl<_VirtKeyState>(this, _$identity);\n\n\n\n@override\nbool operator ==(Object other) {\n  return identical(this, other) || (other.runtimeType == runtimeType&&other is _VirtKeyState&&(identical(other.ctrl, ctrl) || other.ctrl == ctrl)&&(identical(other.alt, alt) || other.alt == alt)&&(identical(other.shift, shift) || other.shift == shift));\n}\n\n\n@override\nint get hashCode => Object.hash(runtimeType,ctrl,alt,shift);\n\n@override\nString toString() {\n  return 'VirtKeyState(ctrl: $ctrl, alt: $alt, shift: $shift)';\n}\n\n\n}\n\n/// @nodoc\nabstract mixin class _$VirtKeyStateCopyWith<$Res> implements $VirtKeyStateCopyWith<$Res> {\n  factory _$VirtKeyStateCopyWith(_VirtKeyState value, $Res Function(_VirtKeyState) _then) = __$VirtKeyStateCopyWithImpl;\n@override @useResult\n$Res call({\n bool ctrl, bool alt, bool shift\n});\n\n\n\n\n}\n/// @nodoc\nclass __$VirtKeyStateCopyWithImpl<$Res>\n    implements _$VirtKeyStateCopyWith<$Res> {\n  __$VirtKeyStateCopyWithImpl(this._self, this._then);\n\n  final _VirtKeyState _self;\n  final $Res Function(_VirtKeyState) _then;\n\n/// Create a copy of VirtKeyState\n/// with the given fields replaced by the non-null parameter values.\n@override @pragma('vm:prefer-inline') $Res call({Object? ctrl = null,Object? alt = null,Object? shift = null,}) {\n  return _then(_VirtKeyState(\nctrl: null == ctrl ? _self.ctrl : ctrl // ignore: cast_nullable_to_non_nullable\nas bool,alt: null == alt ? _self.alt : alt // ignore: cast_nullable_to_non_nullable\nas bool,shift: null == shift ? _self.shift : shift // ignore: cast_nullable_to_non_nullable\nas bool,\n  ));\n}\n\n\n}\n\n// dart format on\n"
  },
  {
    "path": "lib/data/provider/virtual_keyboard.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'virtual_keyboard.dart';\n\n// **************************************************************************\n// RiverpodGenerator\n// **************************************************************************\n\n// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, type=warning\n\n@ProviderFor(VirtKeyboard)\nconst virtKeyboardProvider = VirtKeyboardProvider._();\n\nfinal class VirtKeyboardProvider\n    extends $NotifierProvider<VirtKeyboard, VirtKeyState> {\n  const VirtKeyboardProvider._()\n    : super(\n        from: null,\n        argument: null,\n        retry: null,\n        name: r'virtKeyboardProvider',\n        isAutoDispose: true,\n        dependencies: null,\n        $allTransitiveDependencies: null,\n      );\n\n  @override\n  String debugGetCreateSourceHash() => _$virtKeyboardHash();\n\n  @$internal\n  @override\n  VirtKeyboard create() => VirtKeyboard();\n\n  /// {@macro riverpod.override_with_value}\n  Override overrideWithValue(VirtKeyState value) {\n    return $ProviderOverride(\n      origin: this,\n      providerOverride: $SyncValueProvider<VirtKeyState>(value),\n    );\n  }\n}\n\nString _$virtKeyboardHash() => r'1327d412bfb0dd261f3b555f353a8852b4f753e5';\n\nabstract class _$VirtKeyboard extends $Notifier<VirtKeyState> {\n  VirtKeyState build();\n  @$mustCallSuper\n  @override\n  void runBuild() {\n    final created = build();\n    final ref = this.ref as $Ref<VirtKeyState, VirtKeyState>;\n    final element =\n        ref.element\n            as $ClassProviderElement<\n              AnyNotifier<VirtKeyState, VirtKeyState>,\n              VirtKeyState,\n              Object?,\n              Object?\n            >;\n    element.handleValue(ref, created);\n  }\n}\n"
  },
  {
    "path": "lib/data/res/build_data.dart",
    "content": "// This file is generated by fl_build. Do not edit.\n// ignore_for_file: prefer_single_quotes\n\nabstract class BuildData {\n  static const String name = \"ServerBox\";\n  static const int build = 1331;\n  static const int script = 71;\n}\n"
  },
  {
    "path": "lib/data/res/default.dart",
    "content": "import 'dart:ui';\n\nabstract final class Defaults {\n  static const primaryColor = Color.fromARGB(255, 145, 58, 31);\n\n  static const launchPageIdx = 0;\n\n  static const updateInterval = 3;\n\n  static const editorTheme = 'a11y-light';\n  static const editorDarkTheme = 'monokai';\n}\n"
  },
  {
    "path": "lib/data/res/github_id.dart",
    "content": "abstract final class GithubIds {\n  // Thanks\n  // If you want to change your Github ID, please open an issue.\n  static const contributors = <GhId>{\n    'PaperCube',\n    'GT-610',\n    'Integral-Tech',\n    'its-tom',\n    'leganck',\n    'azkadev',\n    'kalashnikov',\n    'calvinweb',\n    'No06',\n    'QazCetelic',\n    'RainSunMe',\n    'FrancXPT',\n    'Liloupar',\n    'dccif',\n    'mikropsoft',\n    'CakesTwix',\n    'dsvf',\n    'fei1025',\n    'MasedMSD',\n    'GitGitro',\n    'Shin-suechtig',\n    'hypn4'\n  };\n\n  static const participants = <GhId>{\n    'jaychoubaby',\n    'fecture',\n    'Tao173',\n    'Jasonzhu1207',\n    'QingAnLe',\n    'wxdjs',\n    'Aeorq',\n    'allonmymind',\n    'Yuuki-Rin',\n    'LittleState',\n    'karuboniru',\n    'whosphp',\n    'Climit',\n    'dianso',\n    'Jasondeepny',\n    'kaliwell',\n    'ymxkiss',\n    'Ealrang',\n    'hange33',\n    'yuchen1204',\n    'xgzxmytx',\n    'wind057',\n    'a1564471347',\n    'fanzhebufan1',\n    'wcbing',\n    'balh55y',\n    'wc7086',\n    'michaelsara',\n    'xingleiwu',\n    'Cooper098',\n    'xushuojie',\n    'AniberMokie',\n    'LucaLin233',\n    'lalasou',\n    'ohyoxo',\n    'lovechang1986',\n    'luckyreny',\n    'aliuzzz',\n    '58fly',\n    'Potterli20',\n    'Seifon',\n    'bxoooooo',\n    'KatharsisKing',\n    'mervinniu',\n    'L-Super',\n    'Tridays',\n    'Nebulosa-Cat',\n    'dani7959',\n    'MoMingRose',\n    'sakalakagg',\n    'cz32483490',\n    'zj1123581321',\n    'pctoolsx',\n    'pgs666',\n    'FHU-yezi',\n    'ZRY233',\n    'sakuraanzu',\n    'licaon-kter',\n    '77160860',\n    'mijjjj',\n    'muyunil',\n    'Hua159',\n    'jaydong2016',\n    'geol',\n    'Mooling0602',\n    'IllTamer',\n    'marlkiller',\n    'hlarc',\n    'itsandrewpao',\n    'StudyingLover',\n    'QJAG1024',\n    'Wuming-HUST',\n    'WolfCanglong',\n    'liwenjie119',\n    'logce',\n    'h-lyf',\n    '88484396',\n    'honggeigei',\n    'likecreep',\n    'axlrose',\n    'immortal521',\n    'PRO-2684',\n    'Xiaobao-Yang',\n    'Mrhs121',\n    'Fudiautobi',\n    'papaj-na-wrotkach',\n    'kid1412621',\n    'smanx',\n    'xuanyue1024',\n    'RuofengX',\n    'rhwong',\n    'AstroEngineeer',\n    'mochasweet',\n    'back-lacking',\n    'cainiaojr',\n    'MisterMunkerz',\n    'CreeperKong',\n    'zxf945',\n    'cnen2018',\n    'xiaomeng9597',\n    'mingzhao2019',\n    'HHXXYY123',\n    'Lancerys',\n    'yaziku',\n    'yeluosln',\n    'FadeFx',\n    'Snihc1205',\n    'Bjups',\n    '4061N',\n    'itmagpro',\n    'atikattar1104',\n    'coldboy404',\n    'puskyer',\n    'wanababy',\n    'toarujs',\n    'n4vi98',\n    'Android0termux',\n    'idunwannagotoschool',\n    'jiao114514jiao',\n    'jjyou-github',\n    'yeluonight',\n    'Yinhono',\n    'kuvaldini',\n    'aliferne',\n  };\n}\n\ntypedef GhId = String;\n\nextension GhIdX on GhId {\n  String get url => 'https://github.com/$this';\n}\n"
  },
  {
    "path": "lib/data/res/highlight.dart",
    "content": "import 'package:highlight/languages/accesslog.dart';\nimport 'package:highlight/languages/awk.dart';\nimport 'package:highlight/languages/bash.dart';\nimport 'package:highlight/languages/cmake.dart';\nimport 'package:highlight/languages/cpp.dart';\nimport 'package:highlight/languages/css.dart';\nimport 'package:highlight/languages/dart.dart';\nimport 'package:highlight/languages/diff.dart';\nimport 'package:highlight/languages/go.dart';\nimport 'package:highlight/languages/htmlbars.dart';\nimport 'package:highlight/languages/ini.dart';\nimport 'package:highlight/languages/java.dart';\nimport 'package:highlight/languages/javascript.dart';\nimport 'package:highlight/languages/json.dart';\nimport 'package:highlight/languages/kotlin.dart';\nimport 'package:highlight/languages/lisp.dart';\nimport 'package:highlight/languages/lua.dart';\nimport 'package:highlight/languages/nix.dart';\nimport 'package:highlight/languages/objectivec.dart';\nimport 'package:highlight/languages/perl.dart';\nimport 'package:highlight/languages/php.dart';\nimport 'package:highlight/languages/plaintext.dart';\nimport 'package:highlight/languages/powershell.dart';\nimport 'package:highlight/languages/python.dart';\nimport 'package:highlight/languages/ruby.dart';\nimport 'package:highlight/languages/rust.dart';\nimport 'package:highlight/languages/sql.dart';\nimport 'package:highlight/languages/swift.dart';\nimport 'package:highlight/languages/tex.dart';\nimport 'package:highlight/languages/typescript.dart';\nimport 'package:highlight/languages/vim.dart';\nimport 'package:highlight/languages/xml.dart';\nimport 'package:highlight/languages/yaml.dart';\n\nabstract final class Highlights {\n  /// - KEY: fileNameSuffix\n  /// - VAL: highlight\n  static final all = {\n    'dart': dart,\n    'go': go,\n    'rust': rust,\n    'lua': lua,\n    'sh': bash,\n    'py': python,\n    'js': javascript,\n    'ts': typescript,\n    'java': java,\n    'kt': kotlin,\n    'swift': swift,\n    'c': cpp,\n    'oc': objectivec,\n    'ruby': ruby,\n    'perl': perl,\n    'php': php,\n    'nix': nix,\n    'lisp': lisp,\n    'sql': sql,\n    'powershell': powershell,\n    'log': accesslog,\n    'ini': ini,\n    'cmake': cmake,\n    'awk': awk,\n    'json': json,\n    'yaml': yaml,\n    'xml': xml,\n    'cpp': cpp,\n    'diff': diff,\n    'css': css,\n    'html': htmlbars,\n    'tex': tex,\n    'vim': vim,\n    'plaintext': plaintext,\n  };\n\n  static String? getCode(String? fileName) {\n    if (fileName == null) return null;\n    return fileName.split('.').last;\n  }\n}\n"
  },
  {
    "path": "lib/data/res/misc.dart",
    "content": "import 'dart:convert';\n\nabstract final class Miscs {\n  static final blankReg = RegExp(r'\\s+');\n  static final multiBlankreg = RegExp(r'\\s{2,}');\n\n  /// RegExp for password request\n  static final pwdRequestWithUserReg = RegExp(r'\\[sudo\\] password for (.+):');\n\n  /// Private Key max allowed size is 20kb\n  static const privateKeyMaxSize = 20 * 1024;\n\n  /// Editor max allowed size is 1mb\n  static const editorMaxSize = 1024 * 1024;\n\n  /// Max debug log lines\n  static const maxDebugLogLines = 100;\n\n  static const pkgName = 'tech.lolli.toolbox';\n\n  static const jsonEncoder = JsonEncoder.withIndent('  ');\n\n  static const bakFileName = 'srvbox_bak.json';\n}\n"
  },
  {
    "path": "lib/data/res/status.dart",
    "content": "import 'package:server_box/data/model/server/conn.dart';\nimport 'package:server_box/data/model/server/cpu.dart';\nimport 'package:server_box/data/model/server/disk.dart';\nimport 'package:server_box/data/model/server/memory.dart';\nimport 'package:server_box/data/model/server/net_speed.dart';\nimport 'package:server_box/data/model/server/server.dart';\nimport 'package:server_box/data/model/server/system.dart';\nimport 'package:server_box/data/model/server/temp.dart';\n\nabstract final class InitStatus {\n  static SingleCpuCore get _initOneTimeCpuStatus =>\n      SingleCpuCore('cpu', 0, 0, 0, 0, 0, 0, 0);\n  static Cpus get cpus =>\n      Cpus([_initOneTimeCpuStatus], [_initOneTimeCpuStatus]);\n  static NetSpeedPart get _initNetSpeedPart =>\n      NetSpeedPart('', BigInt.zero, BigInt.zero, 0);\n  static NetSpeed get netSpeed =>\n      NetSpeed([_initNetSpeedPart], [_initNetSpeedPart]);\n  static ServerStatus get status => ServerStatus(\n    cpu: cpus,\n    mem: const Memory(total: 1, free: 1, avail: 1),\n    disk: [\n      Disk(\n        path: '/',\n        mount: '/',\n        usedPercent: 0,\n        used: BigInt.zero,\n        size: BigInt.one,\n        avail: BigInt.zero,\n      ),\n    ],\n    tcp: const Conn(maxConn: 0, active: 0, passive: 0, fail: 0),\n    netSpeed: netSpeed,\n    swap: const Swap(total: 0, free: 0, cached: 0),\n    system: SystemType.linux,\n    temps: Temperatures(),\n    diskIO: DiskIO([], []),\n    diskSmart: const [],\n  );\n}\n"
  },
  {
    "path": "lib/data/res/store.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:get_it/get_it.dart';\nimport 'package:server_box/data/store/connection_stats.dart';\nimport 'package:server_box/data/store/container.dart';\nimport 'package:server_box/data/store/history.dart';\nimport 'package:server_box/data/store/private_key.dart';\nimport 'package:server_box/data/store/server.dart';\nimport 'package:server_box/data/store/setting.dart';\nimport 'package:server_box/data/store/snippet.dart';\n\nfinal GetIt getIt = GetIt.instance;\n\nabstract final class Stores {\n  static SettingStore get setting => getIt<SettingStore>();\n  static ServerStore get server => getIt<ServerStore>();\n  static ContainerStore get container => getIt<ContainerStore>();\n  static PrivateKeyStore get key => getIt<PrivateKeyStore>();\n  static SnippetStore get snippet => getIt<SnippetStore>();\n  static HistoryStore get history => getIt<HistoryStore>();\n  // Keep the legacy box registered so existing connection stats DB files remain intact.\n  static ConnectionStatsStore get connectionStats => getIt<ConnectionStatsStore>();\n\n  /// All stores that need backup\n  static List<HiveStore> get _allBackup => [\n        setting,\n        server,\n        container,\n        key,\n        snippet,\n        history,\n        connectionStats,\n      ];\n\n  static Future<void> init() async {\n    getIt.registerLazySingleton<SettingStore>(() => SettingStore.instance);\n    getIt.registerLazySingleton<ServerStore>(() => ServerStore.instance);\n    getIt.registerLazySingleton<ContainerStore>(() => ContainerStore.instance);\n    getIt.registerLazySingleton<PrivateKeyStore>(() => PrivateKeyStore.instance);\n    getIt.registerLazySingleton<SnippetStore>(() => SnippetStore.instance);\n    getIt.registerLazySingleton<HistoryStore>(() => HistoryStore.instance);\n    getIt.registerLazySingleton<ConnectionStatsStore>(() => ConnectionStatsStore.instance);\n    \n    await Future.wait(_allBackup.map((store) => store.init()));\n  }\n\n  static int get lastModTime {\n    var lastModTime = 0;\n    for (final store in _allBackup) {\n      final last = store.lastUpdateTs;\n      if (last == null) {\n        continue;\n      }\n      var lastModTimeTs = 0;\n      for (final item in last.entries) {\n        final ts = item.value;\n        if (ts > lastModTimeTs) {\n          lastModTimeTs = ts;\n        }\n      }\n      if (lastModTimeTs > lastModTime) {\n        lastModTime = lastModTimeTs;\n      }\n    }\n    return lastModTime;\n  }\n}\n"
  },
  {
    "path": "lib/data/res/terminal.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:xterm/ui.dart';\n\nabstract final class TerminalThemes {\n  static const dark = TerminalTheme(\n    cursor: Color.fromARGB(137, 174, 175, 173),\n    selectionCursor: Color(0xff8b2252),\n    selection: Color.fromARGB(147, 174, 175, 173),\n    foreground: Color(0XFFCCCCCC),\n    background: Colors.black,\n    searchHitBackground: Color(0XFFFFFF2B),\n    searchHitBackgroundCurrent: Color(0XFF31FF26),\n    searchHitForeground: Color(0XFF000000),\n    red: Color.fromARGB(255, 194, 54, 33),\n    green: Color.fromARGB(255, 37, 188, 36),\n    yellow: Color.fromARGB(255, 173, 173, 39),\n    blue: Color.fromARGB(255, 73, 46, 225),\n    magenta: Color.fromARGB(255, 211, 56, 211),\n    cyan: Color.fromARGB(255, 51, 187, 200),\n    white: Color.fromARGB(255, 203, 204, 205),\n    brightBlack: Color.fromARGB(255, 129, 131, 131),\n    brightRed: Color.fromARGB(255, 252, 57, 31),\n    brightGreen: Color.fromARGB(255, 49, 231, 34),\n    brightYellow: Color.fromARGB(255, 234, 236, 35),\n    brightBlue: Color.fromARGB(255, 88, 51, 255),\n    brightMagenta: Color.fromARGB(255, 249, 53, 248),\n    brightCyan: Color.fromARGB(255, 20, 240, 240),\n    brightWhite: Color.fromARGB(255, 233, 235, 235),\n    black: Colors.black,\n  );\n  static const light = TerminalTheme(\n    cursor: Color.fromARGB(153, 174, 175, 173),\n    selectionCursor: Color(0xff8b2252),\n    selection: Color.fromARGB(102, 174, 175, 173),\n    foreground: Color(0XFF000000),\n    background: Color(0XFFFFFFFF),\n    searchHitBackground: Color(0XFFFFFF2B),\n    searchHitBackgroundCurrent: Color(0XFF31FF26),\n    searchHitForeground: Color(0XFF000000),\n    red: Color.fromARGB(255, 194, 54, 33),\n    green: Color.fromARGB(255, 37, 188, 36),\n    yellow: Color.fromARGB(255, 173, 173, 39),\n    blue: Color.fromARGB(255, 73, 46, 225),\n    magenta: Color.fromARGB(255, 211, 56, 211),\n    cyan: Color.fromARGB(255, 51, 187, 200),\n    white: Color.fromARGB(255, 203, 204, 205),\n    brightBlack: Color.fromARGB(255, 129, 131, 131),\n    brightRed: Color.fromARGB(255, 252, 57, 31),\n    brightGreen: Color.fromARGB(255, 49, 231, 34),\n    brightYellow: Color.fromARGB(255, 234, 236, 35),\n    brightBlue: Color.fromARGB(255, 88, 51, 255),\n    brightMagenta: Color.fromARGB(255, 249, 53, 248),\n    brightCyan: Color.fromARGB(255, 20, 240, 240),\n    brightWhite: Color.fromARGB(255, 233, 235, 235),\n    black: Colors.black,\n  );\n}\n\nextension TerminalThemeX on TerminalTheme {\n  TerminalTheme copyWith({\n    Color? cursor,\n    Color? selectionCursor,\n    Color? selection,\n    Color? foreground,\n    Color? background,\n    Color? searchHitBackground,\n    Color? searchHitBackgroundCurrent,\n    Color? searchHitForeground,\n    Color? red,\n    Color? green,\n    Color? yellow,\n    Color? blue,\n    Color? magenta,\n    Color? cyan,\n    Color? white,\n    Color? brightBlack,\n    Color? brightRed,\n    Color? brightGreen,\n    Color? brightYellow,\n    Color? brightBlue,\n    Color? brightMagenta,\n    Color? brightCyan,\n    Color? brightWhite,\n    Color? black,\n  }) {\n    return TerminalTheme(\n      cursor: cursor ?? this.cursor,\n      selectionCursor: selectionCursor ?? this.selectionCursor,\n      selection: selection ?? this.selection,\n      foreground: foreground ?? this.foreground,\n      background: background ?? this.background,\n      searchHitBackground: searchHitBackground ?? this.searchHitBackground,\n      searchHitBackgroundCurrent:\n          searchHitBackgroundCurrent ?? this.searchHitBackgroundCurrent,\n      searchHitForeground: searchHitForeground ?? this.searchHitForeground,\n      red: red ?? this.red,\n      green: green ?? this.green,\n      yellow: yellow ?? this.yellow,\n      blue: blue ?? this.blue,\n      magenta: magenta ?? this.magenta,\n      cyan: cyan ?? this.cyan,\n      white: white ?? this.white,\n      brightBlack: brightBlack ?? this.brightBlack,\n      brightRed: brightRed ?? this.brightRed,\n      brightGreen: brightGreen ?? this.brightGreen,\n      brightYellow: brightYellow ?? this.brightYellow,\n      brightBlue: brightBlue ?? this.brightBlue,\n      brightMagenta: brightMagenta ?? this.brightMagenta,\n      brightCyan: brightCyan ?? this.brightCyan,\n      brightWhite: brightWhite ?? this.brightWhite,\n      black: black ?? this.black,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/data/res/url.dart",
    "content": "abstract final class Urls {\n  static const cdnBase = 'https://cdn.lpkt.cn/serverbox';\n  static const updateCfg = '$cdnBase/update2.json';\n  static const myGithub = 'https://github.com/lollipopkit';\n  static const thisRepo = '$myGithub/flutter_server_box';\n  static const appHelp = '$thisRepo#-help';\n  static const appWiki = '$thisRepo/wiki';\n}\n"
  },
  {
    "path": "lib/data/ssh/session_manager.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:server_box/core/chan.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\n\nenum TermSessionStatus {\n  connecting,\n  connected,\n  disconnected;\n\n  @override\n  String toString() {\n    return name.capitalize;\n  }\n}\n\n/// Represents a running SSH terminal session for Android notifications and iOS Live Activities.\nclass TermSessionInfo {\n  final String id;\n  final String title; // e.g. server name\n  final String subtitle; // e.g. user@ip:port\n  final int startTimeMs;\n  final TermSessionStatus status;\n\n  TermSessionInfo({\n    required this.id,\n    required this.title,\n    required this.subtitle,\n    required this.startTimeMs,\n    required this.status,\n  });\n\n  Map<String, Object> toJson() => {\n    'id': id,\n    'title': title,\n    'subtitle': subtitle,\n    'startTimeMs': startTimeMs,\n    'status': status.toString(),\n  };\n}\n\n/// Singleton to track active SSH sessions and sync to Android notifications.\nabstract final class TermSessionManager {\n  static final Map<String, _Entry> _entries = {};\n  static String? _activeId; // For iOS Live Activity\n  static Timer? _updateTimer; // Timer for iOS Live Activity updates\n  static const _updateInterval = Duration(seconds: 5); // 5-second update interval\n\n  static void init() {\n    if (isAndroid) {\n      MethodChans.registerHandler(\n        (id) async {\n          _entries[id]?.disconnect?.call();\n        }, \n        () {\n          // Stop all connections when notification \"Stop All\" is pressed\n          stopAllConnections();\n        },\n      );\n    }\n  }\n\n  /// Called when Android notification \"Stop All\" button is pressed\n  static void stopAllConnections() {\n    // Disconnect all sessions\n    final disconnectCallbacks = _entries.values.map((e) => e.disconnect).where((cb) => cb != null).toList();\n    for (final disconnect in disconnectCallbacks) {\n      disconnect!();\n    }\n    // Clear all entries\n    _entries.clear();\n    _activeId = null;\n    _sync();\n  }\n\n  /// Add a session record and push update to Android.\n  static void add({\n    required String id,\n    required Spi spi,\n    required int startTimeMs,\n    required VoidCallback disconnect,\n    TermSessionStatus status = TermSessionStatus.connecting,\n  }) {\n    final info = TermSessionInfo(\n      id: id,\n      title: spi.name,\n      subtitle: spi.oldId,\n      startTimeMs: startTimeMs,\n      status: status,\n    );\n    _entries[id] = _Entry(info, disconnect, hasTerminalUI: true);\n    _activeId = id; // most recent as active\n    _sync();\n  }\n\n  static void updateStatus(String id, TermSessionStatus status) {\n    final old = _entries[id];\n    if (old == null) return;\n    _entries[id] = _Entry(\n      TermSessionInfo(\n        id: old.info.id,\n        title: old.info.title,\n        subtitle: old.info.subtitle,\n        startTimeMs: old.info.startTimeMs,\n        status: status,\n      ),\n      old.disconnect,\n      hasTerminalUI: old.hasTerminalUI,\n    );\n    _sync();\n  }\n\n  static void remove(String id) {\n    _entries.remove(id);\n    if (_activeId == id) {\n      _activeId = _entries.keys.firstOrNull;\n    }\n    _sync();\n  }\n\n  static Future<void> _sync() async {\n    // Android: update foreground service notifications\n    if (isAndroid) {\n      final isRunning = await MethodChans.isServiceRunning();\n      if (_entries.isEmpty) {\n        if (isRunning) {\n          MethodChans.stopService();\n        }\n        await MethodChans.updateSessions(jsonEncode({'sessions': []}));\n      } else {\n        if (!isRunning) {\n          MethodChans.startService();\n        }\n        final payload = jsonEncode({'sessions': _entries.values.map((e) => e.info.toJson()).toList()});\n        await MethodChans.updateSessions(payload);\n      }\n    }\n\n    // iOS: manage Live Activity timer\n    if (isIOS) {\n      if (_entries.isEmpty) {\n        _updateTimer?.cancel();\n        _updateTimer = null;\n        await MethodChans.stopLiveActivity();\n      } else {\n        // Start timer if not already running\n        _updateTimer ??= Timer.periodic(_updateInterval, (_) => _updateLiveActivity());\n        // Immediately update for immediate feedback\n        await _updateLiveActivity();\n      }\n    }\n  }\n\n  static Future<void> _updateLiveActivity() async {\n    if (!isIOS || _entries.isEmpty) return;\n\n    final connectionCount = _entries.length;\n\n    if (connectionCount == 1) {\n      // Single connection: show hostname\n      final id = _activeId ?? _entries.keys.first;\n      final entry = _entries[id];\n      if (entry == null) return;\n      final payload = jsonEncode({\n        ...entry.info.toJson(),\n        'hasTerminal': entry.hasTerminalUI,\n        'connectionCount': connectionCount,\n      });\n      await MethodChans.updateLiveActivity(payload);\n    } else {\n      // Multiple connections: show connection count\n      final id = _activeId ?? _entries.keys.first;\n      final entry = _entries[id];\n      if (entry == null) return;\n      final payload = jsonEncode({\n        'id': 'multi_connections',\n        'title': '$connectionCount connections',\n        'subtitle': 'Multiple SSH sessions active',\n        'startTimeMs': entry.info.startTimeMs,\n        'status': TermSessionStatus.connected.toString(),\n        'hasTerminal': entry.hasTerminalUI,\n        'connectionCount': connectionCount,\n      });\n      await MethodChans.updateLiveActivity(payload);\n    }\n  }\n\n  /// Mark which session is actively displayed in UI (for iOS Live Activity).\n  static void setActive(String id, {bool hasTerminal = true}) {\n    _activeId = id;\n    final old = _entries[id];\n    if (old != null) {\n      _entries[id] = _Entry(old.info, old.disconnect, hasTerminalUI: hasTerminal);\n      _sync();\n    }\n  }\n\n  /// Stop Live Activity when app is closed/terminated (iOS only).\n  static Future<void> stopLiveActivityOnAppClose() async {\n    if (!isIOS) return;\n\n    // Cancel any running timers\n    _updateTimer?.cancel();\n    _updateTimer = null;\n\n    // Stop the Live Activity\n    await MethodChans.stopLiveActivity();\n  }\n}\n\nclass _Entry {\n  final TermSessionInfo info;\n  final VoidCallback? disconnect;\n  final bool hasTerminalUI;\n  _Entry(this.info, this.disconnect, {this.hasTerminalUI = true});\n}\n"
  },
  {
    "path": "lib/data/store/connection_stats.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/server/connection_stat.dart';\n\nclass ConnectionStatsStore extends HiveStore {\n  ConnectionStatsStore._() : super('connection_stats');\n  \n  static final instance = ConnectionStatsStore._();\n  \n  // Record a connection attempt\n  void recordConnection(ConnectionStat stat) {\n    final key = '${stat.serverId}_${ShortId.generate()}';\n    set(key, stat);\n    _cleanOldRecords(stat.serverId);\n  }\n  \n  // Clean records older than 30 days for a specific server\n  void _cleanOldRecords(String serverId) {\n    final cutoffTime = DateTime.now().subtract(const Duration(days: 30));\n    final allKeys = keys().toList();\n    final keysToDelete = <String>[];\n    \n    for (final key in allKeys) {\n      if (key.startsWith(serverId)) {\n        final parts = key.split('_');\n        if (parts.length >= 2) {\n          final timestamp = int.tryParse(parts.last);\n          if (timestamp != null) {\n            final recordTime = DateTime.fromMillisecondsSinceEpoch(timestamp);\n            if (recordTime.isBefore(cutoffTime)) {\n              keysToDelete.add(key);\n            }\n          }\n        }\n      }\n    }\n    \n    for (final key in keysToDelete) {\n      remove(key);\n    }\n  }\n  \n  // Get connection stats for a specific server\n  ServerConnectionStats getServerStats(String serverId, String serverName) {\n    final allStats = getConnectionHistory(serverId);\n    \n    if (allStats.isEmpty) {\n      return ServerConnectionStats(\n        serverId: serverId,\n        serverName: serverName,\n        totalAttempts: 0,\n        successCount: 0,\n        failureCount: 0,\n        recentConnections: [],\n        successRate: 0.0,\n      );\n    }\n    \n    final totalAttempts = allStats.length;\n    final successCount = allStats.where((s) => s.result.isSuccess).length;\n    final failureCount = totalAttempts - successCount;\n    final successRate = totalAttempts > 0 ? (successCount / totalAttempts) : 0.0;\n    \n    final successTimes = allStats\n        .where((s) => s.result.isSuccess)\n        .map((s) => s.timestamp)\n        .toList();\n    final failureTimes = allStats\n        .where((s) => !s.result.isSuccess)\n        .map((s) => s.timestamp)\n        .toList();\n    \n    DateTime? lastSuccessTime;\n    DateTime? lastFailureTime;\n    \n    if (successTimes.isNotEmpty) {\n      successTimes.sort((a, b) => b.compareTo(a));\n      lastSuccessTime = successTimes.first;\n    }\n    \n    if (failureTimes.isNotEmpty) {\n      failureTimes.sort((a, b) => b.compareTo(a));\n      lastFailureTime = failureTimes.first;\n    }\n    \n    // Get recent connections (last 20)\n    final recentConnections = allStats.take(20).toList();\n    \n    return ServerConnectionStats(\n      serverId: serverId,\n      serverName: serverName,\n      totalAttempts: totalAttempts,\n      successCount: successCount,\n      failureCount: failureCount,\n      lastSuccessTime: lastSuccessTime,\n      lastFailureTime: lastFailureTime,\n      recentConnections: recentConnections,\n      successRate: successRate,\n    );\n  }\n  \n  // Get connection history for a specific server\n  List<ConnectionStat> getConnectionHistory(String serverId) {\n    final allKeys = keys().where((key) => key.startsWith(serverId)).toList();\n    final stats = <ConnectionStat>[];\n    \n    for (final key in allKeys) {\n      final stat = get<ConnectionStat>(\n        key,\n        fromObj: (val) {\n          if (val is ConnectionStat) return val;\n          if (val is Map<dynamic, dynamic>) {\n            final map = val.toStrDynMap;\n            if (map == null) return null;\n            try {\n              return ConnectionStat.fromJson(map as Map<String, dynamic>);\n            } catch (e) {\n              dprint('Parsing ConnectionStat from JSON', e);\n            }\n          }\n          return null;\n        },\n      );\n      if (stat != null) {\n        stats.add(stat);\n      }\n    }\n    \n    // Sort by timestamp, newest first\n    stats.sort((a, b) => b.timestamp.compareTo(a.timestamp));\n    return stats;\n  }\n  \n  // Get all servers' stats\n  List<ServerConnectionStats> getAllServerStats() {\n    final serverIds = <String>{};\n    final serverNames = <String, String>{};\n    \n    // Get all unique server IDs\n    for (final key in keys()) {\n      final parts = key.split('_');\n      if (parts.length >= 2) {\n        final serverId = parts[0];\n        serverIds.add(serverId);\n        \n        // Try to get server name from the stored stat\n        final stat = get<ConnectionStat>(\n          key,\n          fromObj: (val) {\n            if (val is ConnectionStat) return val;\n            if (val is Map<dynamic, dynamic>) {\n              final map = val.toStrDynMap;\n              if (map == null) return null;\n              try {\n                return ConnectionStat.fromJson(map as Map<String, dynamic>);\n              } catch (e) {\n                dprint('Parsing ConnectionStat from JSON', e);\n              }\n            }\n            return null;\n          },\n        );\n        if (stat != null) {\n          serverNames[serverId] = stat.serverName;\n        }\n      }\n    }\n    \n    final allStats = <ServerConnectionStats>[];\n    for (final serverId in serverIds) {\n      final serverName = serverNames[serverId] ?? serverId;\n      final stats = getServerStats(serverId, serverName);\n      allStats.add(stats);\n    }\n    \n    return allStats;\n  }\n  \n  // Clear all connection stats\n  void clearAll() {\n    box.clear();\n  }\n  \n  // Clear stats for a specific server\n  void clearServerStats(String serverId) {\n    final keysToDelete = keys().where((key) {\n      if (key == serverId) return true;\n      return key.startsWith('${serverId}_');\n    }).toList();\n    for (final key in keysToDelete) {\n      remove(key);\n    }\n  }\n\n  Future<void> compact() async {\n    Loggers.app.info('Start compacting connection_stats database...');\n    try {\n      await box.compact();\n      Loggers.app.info('Finished compacting connection_stats database');\n    } catch (e, st) {\n      Loggers.app.warning('Failed compacting connection_stats database', e, st);\n      rethrow;\n    }\n  }\n}"
  },
  {
    "path": "lib/data/store/container.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/container/type.dart';\nimport 'package:server_box/data/res/store.dart';\n\nconst _keyConfig = 'providerConfig';\n\nclass ContainerStore extends HiveStore {\n  ContainerStore._() : super('docker');\n\n  static final instance = ContainerStore._();\n\n  String? fetch(String? id) {\n    return box.get(id);\n  }\n\n  void put(String id, String host) {\n    set(id, host);\n  }\n\n  ContainerType getType([String id = '']) {\n    final cfg = box.get(_keyConfig + id);\n    if (cfg != null) {\n      final type = ContainerType.values.firstWhereOrNull((e) => e.toString() == cfg);\n      if (type != null) return type;\n    }\n\n    return defaultType;\n  }\n\n  ContainerType get defaultType {\n    if (Stores.setting.usePodman.get()) return ContainerType.podman;\n    return ContainerType.docker;\n  }\n\n  void setType(ContainerType type, [String id = '']) {\n    if (type == defaultType) {\n      // box.delete(_keyConfig + id);\n      remove(_keyConfig + id);\n    } else {\n      // box.put(_keyConfig + id, type.toString());\n      set(_keyConfig + id, type.toString());\n    }\n  }\n}\n"
  },
  {
    "path": "lib/data/store/history.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:hive_ce_flutter/hive_flutter.dart';\n\n/// index from 0 -> n : latest -> oldest\nclass _ListHistory {\n  final List _history;\n  final String _name;\n  final Box _box;\n\n  _ListHistory({required Box box, required String name})\n    : _box = box,\n      _name = name,\n      _history = box.get(name, defaultValue: [])!;\n\n  void add(String path) {\n    _history.remove(path);\n    _history.insert(0, path);\n    _box.put(_name, _history);\n  }\n\n  List get all => _history;\n}\n\nclass _MapHistory {\n  final Map _history;\n  final String _name;\n  final Box _box;\n\n  _MapHistory({required Box box, required String name})\n    : _box = box,\n      _name = name,\n      _history = box.get(name, defaultValue: <dynamic, dynamic>{})!;\n\n  void put(String id, String val) {\n    _history[id] = val;\n    _box.put(_name, _history);\n  }\n\n  String? fetch(String id) => _history[id];\n}\n\nclass HistoryStore extends HiveStore {\n  HistoryStore._() : super('history');\n\n  static final instance = HistoryStore._();\n\n  /// Paths that user has visited by 'Locate' button\n  late final sftpGoPath = _ListHistory(box: box, name: 'sftpPath');\n\n  late final sftpLastPath = _MapHistory(box: box, name: 'sftpLastPath');\n\n  late final sshCmds = _ListHistory(box: box, name: 'sshCmds');\n\n  /// Notify users that this app will write script to server to works properly\n  late final writeScriptTipShown = propertyDefault('writeScriptTipShown', false);\n}\n"
  },
  {
    "path": "lib/data/store/private_key.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\n\nimport 'package:server_box/data/model/server/private_key_info.dart';\n\nclass PrivateKeyStore extends HiveStore {\n  PrivateKeyStore._() : super('key');\n\n  static final instance = PrivateKeyStore._();\n\n  void put(PrivateKeyInfo info) {\n    set(info.id, info);\n  }\n\n  List<PrivateKeyInfo> fetch() {\n    final ps = <PrivateKeyInfo>[];\n    for (final key in keys()) {\n      final s = get<PrivateKeyInfo>(\n        key,\n        fromObj: (val) {\n          if (val is PrivateKeyInfo) return val;\n          if (val is Map<dynamic, dynamic>) {\n            final map = val.toStrDynMap;\n            if (map == null) return null;\n            try {\n              final pki = PrivateKeyInfo.fromJson(map as Map<String, dynamic>);\n              put(pki);\n              return pki;\n            } catch (e) {\n              dprint('Parsing PrivateKeyInfo from JSON', e);\n            }\n          }\n          return null;\n        },\n      );\n      if (s != null) {\n        ps.add(s);\n      }\n    }\n    return ps;\n  }\n\n  PrivateKeyInfo? fetchOne(String? id) {\n    if (id == null) return null;\n    return box.get(id);\n  }\n\n  void delete(PrivateKeyInfo s) {\n    remove(s.id);\n  }\n}\n"
  },
  {
    "path": "lib/data/store/server.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\n\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/store/container.dart';\nimport 'package:server_box/data/store/setting.dart';\nimport 'package:server_box/data/store/snippet.dart';\n\nclass ServerStore extends HiveStore {\n  ServerStore._() : super('server');\n\n  static final instance = ServerStore._();\n\n  void put(Spi info) {\n    set(info.id, info);\n  }\n\n  List<Spi> fetch() {\n    final List<Spi> ss = [];\n    for (final id in keys()) {\n      final s = get<Spi>(\n        id,\n        fromObj: (val) {\n          if (val is Spi) return val;\n          if (val is Map<dynamic, dynamic>) {\n            final map = val.toStrDynMap;\n            if (map == null) return null;\n            try {\n              final spi = Spi.fromJson(map as Map<String, dynamic>);\n              put(spi);\n              return spi;\n            } catch (e) {\n              dprint('Parsing Spi from JSON', e);\n            }\n          }\n          return null;\n        },\n      );\n      if (s != null) {\n        ss.add(s);\n      }\n    }\n    return ss;\n  }\n\n  void delete(String id) {\n    remove(id);\n  }\n\n  void update(Spi old, Spi newInfo) {\n    if (!have(old)) {\n      throw Exception('Old spi: $old not found');\n    }\n    delete(old.id);\n    put(newInfo);\n  }\n\n  bool have(Spi s) => get(s.id) != null;\n\n  void migrateIds() {\n    final ss = fetch();\n    final idMap = <String, String>{};\n\n    // Collect all old to new ID mappings\n    for (final s in ss) {\n      final newId = s.migrateId();\n      if (newId == null) continue;\n      // Use s.oldId as the key, because s.id would be empty for a server being migrated.\n      // s.oldId represents the identifier used before migration.\n      idMap[s.oldId] = newId;\n    }\n\n    final srvOrder = SettingStore.instance.serverOrder.fetch();\n    final snippets = SnippetStore.instance.fetch();\n    final container = ContainerStore.instance;\n\n    bool srvOrderChanged = false;\n    // Update all references to the servers\n    for (final e in idMap.entries) {\n      final oldId = e.key;\n      final newId = e.value;\n\n      // Replace ids in ordering settings.\n      final srvIdx = srvOrder.indexOf(oldId);\n      if (srvIdx != -1) {\n        srvOrder[srvIdx] = newId;\n        srvOrderChanged = true;\n      }\n\n      // Replace ids in jump server settings.\n      final spi = get<Spi>(newId);\n      if (spi != null) {\n        final jumpId = spi.jumpId; // This could be an oldId.\n        // Check if this jumpId corresponds to a server that was also migrated.\n        if (jumpId != null && idMap.containsKey(jumpId)) {\n          final newJumpId = idMap[jumpId];\n          if (spi.jumpId != newJumpId) {\n            final newSpi = spi.copyWith(jumpId: newJumpId);\n            update(spi, newSpi);\n          }\n        }\n      }\n\n      // Replace ids in [Snippet]\n      for (final snippet in snippets) {\n        final autoRunsOn = snippet.autoRunOn;\n        final idx = autoRunsOn?.indexOf(oldId);\n        if (idx != null && idx != -1) {\n          final newAutoRunsOn = List<String>.from(autoRunsOn ?? []);\n          newAutoRunsOn[idx] = newId;\n          final newSnippet = snippet.copyWith(autoRunOn: newAutoRunsOn);\n          SnippetStore.instance.update(snippet, newSnippet);\n        }\n      }\n\n      // Replace ids in [Container]\n      final dockerHost = container.fetch(oldId);\n      if (dockerHost != null) {\n        container.remove(oldId);\n        container.set(newId, dockerHost);\n      }\n    }\n\n    if (srvOrderChanged) {\n      SettingStore.instance.serverOrder.put(srvOrder);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/data/store/setting.dart",
    "content": "import 'dart:convert';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:server_box/data/model/app/menu/server_func.dart';\nimport 'package:server_box/data/model/app/net_view.dart';\nimport 'package:server_box/data/model/app/server_detail_card.dart';\nimport 'package:server_box/data/model/app/tab.dart';\nimport 'package:server_box/data/model/ssh/virtual_key.dart';\nimport 'package:server_box/data/res/default.dart';\n\nclass SettingStore extends HiveStore {\n  SettingStore._() : super('setting');\n\n  static final instance = SettingStore._();\n\n  /// Time out for server connect and more...\n  late final timeout = propertyDefault('timeOut', 5);\n\n  /// Record history of SFTP path and etc.\n  late final recordHistory = propertyDefault('recordHistory', true);\n\n  /// Lanch page idx\n  // late final launchPage = property('launchPage', Defaults.launchPageIdx);\n\n  /// Disk view: amount / IO\n  late final serverTabPreferDiskAmount = propertyDefault('serverTabPreferDiskAmount', false);\n\n  /// Bigger for bigger font size\n  /// 1.0 means 100%\n  /// Warning: This may cause some UI issues\n  late final textFactor = propertyDefault('textFactor', 1.0);\n\n  /// The seed of color scheme\n  late final colorSeed = propertyDefault('primaryColor', 4287106639);\n\n  late final serverStatusUpdateInterval = propertyDefault(\n    'serverStatusUpdateInterval',\n    Defaults.updateInterval,\n  );\n\n  // Max retry count when connect to server\n  late final maxRetryCount = propertyDefault('maxRetryCount', 2);\n\n  // Night mode: 0 -> auto, 1 -> light, 2 -> dark, 3 -> AMOLED, 4 -> AUTO-AMOLED\n  late final themeMode = propertyDefault('themeMode', 0);\n\n  // Font file path\n  late final fontPath = propertyDefault('fontPath', '');\n\n  // Backgroud running (Android)\n  late final bgRun = propertyDefault('bgRun', isAndroid);\n\n  // Server order\n  late final serverOrder = listProperty<String>('serverOrder');\n\n  late final snippetOrder = listProperty<String>('snippetOrder');\n\n  // Server details page cards order\n  late final detailCardOrder = listProperty(\n    'detailCardOrder',\n    defaultValue: ServerDetailCards.values.map((e) => e.name).toList(),\n  );\n\n  // Disabled detail cards (for persistence when toggling visibility)\n  late final detailCardDisabled = listProperty<String>('detailCardDisabled');\n\n  // Disabled SSH virtual keys (for persistence when toggling visibility)\n  late final sshVirtKeysDisabled = listProperty<int>('sshVirtKeysDisabled');\n\n  // SSH term font size\n  late final termFontSize = propertyDefault('termFontSize', 13.0);\n\n  // Locale\n  late final locale = propertyDefault('locale', '');\n\n  // SSH virtual key (ctrl | alt) auto turn off\n  late final sshVirtualKeyAutoOff = propertyDefault('sshVirtualKeyAutoOff', true);\n\n  late final editorFontSize = propertyDefault('editorFontSize', 12.5);\n\n  late final editorFontFamily = propertyDefault('editorFontFamily', '');\n\n  /// Trusted SSH host key fingerprints keyed by `serverId::keyType`.\n  late final sshKnownHostFingerprints = propertyDefault<Map<String, String>>(\n    'sshKnownHostFingerprints',\n    const {},\n    fromObj: (raw) {\n      if (raw is Map) {\n        return raw.map((key, value) => MapEntry(key.toString(), value.toString()));\n      }\n      return <String, String>{};\n    },\n  );\n\n  // Editor theme\n  late final editorTheme = propertyDefault('editorTheme', Defaults.editorTheme);\n\n  late final editorDarkTheme = propertyDefault('editorDarkTheme', Defaults.editorDarkTheme);\n\n  late final fullScreen = propertyDefault('fullScreen', false);\n\n  late final fullScreenJitter = propertyDefault('fullScreenJitter', true);\n\n  // late final fullScreenRotateQuarter = property(\n  //   'fullScreenRotateQuarter',\n  //   1,\n  // );\n\n  // late final keyboardType = property(\n  //   'keyboardType',\n  //   TextInputType.text.index,\n  // );\n\n  late final sshVirtKeys = listProperty<int>(\n    'sshVirtKeys',\n    defaultValue: VirtKeyX.defaultOrder.map((e) => e.index).toList(),\n    fromObj: (val) => List<int>.from(val as List),\n  );\n\n  late final netViewType = propertyDefault(\n    'netViewType',\n    NetViewType.speed,\n    fromObj: (val) => NetViewType.values.firstWhereOrNull((e) => e.name == val),\n    toObj: (type) => type?.name,\n  );\n\n  // Only valid on iOS\n  late final autoUpdateHomeWidget = propertyDefault('autoUpdateHomeWidget', isIOS);\n\n  late final autoCheckAppUpdate = propertyDefault('autoCheckAppUpdate', true);\n\n  /// Display server tab function buttons on the bottom of each server card if [true]\n  ///\n  /// Otherwise, display them on the top of server detail page\n  late final moveServerFuncs = propertyDefault('moveOutServerTabFuncBtns', false);\n\n  /// Whether use `rm -r` to delete directory on SFTP\n  late final sftpRmrDir = propertyDefault('sftpRmrDir', false);\n\n  /// Whether use system's primary color as the app's primary color\n  late final useSystemPrimaryColor = propertyDefault('useSystemPrimaryColor', false);\n\n  /// Only valid on iOS / Android / Windows\n  late final useBioAuth = propertyDefault('useBioAuth', false);\n\n  /// Delay to lock the App with BioAuth, in seconds.\n  /// Set to `0` to disable this feature.\n  late final delayBioAuthLock = propertyDefault('delayBioAuthLock', 0);\n\n  /// The performance of highlight is bad\n  late final editorHighlight = propertyDefault('editorHighlight', true);\n\n  /// Open SFTP with last viewed path\n  late final sftpOpenLastPath = propertyDefault('sftpOpenLastPath', true);\n\n  /// Show folders first in SFTP file browser\n  late final sftpShowFoldersFirst = propertyDefault('sftpShowFoldersFirst', true);\n\n  /// Show tip of suspend\n  late final showSuspendTip = propertyDefault('showSuspendTip', true);\n\n  /// Whether collapse UI items by default\n  late final collapseUIDefault = propertyDefault('collapseUIDefault', true);\n\n  /// Terminal AI helper configuration\n  late final askAiBaseUrl = propertyDefault('askAiBaseUrl', 'https://api.openai.com');\n  late final askAiApiKey = propertyDefault('askAiApiKey', '');\n  late final askAiModel = propertyDefault('askAiModel', 'gpt-4o-mini');\n\n  late final serverFuncBtns = listProperty('serverBtns', defaultValue: ServerFuncBtn.defaultIdxs);\n\n  /// Docker is more popular than podman, set to `false` to use docker\n  late final usePodman = propertyDefault('usePodman', false);\n\n  /// Try to use `sudo` to run docker command\n  late final containerTrySudo = propertyDefault('containerTrySudo', true);\n\n  /// Keep previous server status when err occurs\n  late final keepStatusWhenErr = propertyDefault('keepStatusWhenErr', false);\n\n  /// Parse container stat\n  late final containerParseStat = propertyDefault('containerParseStat', true);\n\n  /// Auto refresh container status\n  late final containerAutoRefresh = propertyDefault('containerAutoRefresh', true);\n\n  /// Use double column servers page on Desktop\n  late final doubleColumnServersPage = propertyDefault('doubleColumnServersPage', true);\n\n  /// Ignore local network device (eg: br-xxx, ovs-system...)\n  /// when building traffic view on server tab\n  //late final ignoreLocalNet = propertyDefault('ignoreLocalNet', true);\n\n  /// Remerber pwd in memory\n  /// Used for [DialogX.showPwdDialog]\n  late final rememberPwdInMem = propertyDefault('rememberPwdInMem', true);\n\n  /// SSH Term Theme\n  /// 0: follow app theme, 1: light, 2: dark\n  late final termTheme = propertyDefault('termTheme', 0);\n\n  /// Compatiablity for Chinese Android.\n  /// Set it to true, if you use Safe Keyboard on Chinese Android\n  // late final cnKeyboardComp = propertyDefault('cnKeyboardComp', false);\n\n  late final lastVer = propertyDefault('lastVer', 0);\n\n  /// Hide title bar on desktop\n  late final hideTitleBar = propertyDefault('hideTitleBar', isDesktop);\n\n  /// Display CPU view as progress, also called as old CPU view\n  late final cpuViewAsProgress = propertyDefault('cpuViewAsProgress', false);\n\n  late final displayCpuIndex = propertyDefault('displayCpuIndex', true);\n\n  late final editorSoftWrap = propertyDefault('editorSoftWrap', isIOS);\n\n  late final sshTermHelpShown = propertyDefault('sshTermHelpShown', false);\n\n  late final horizonVirtKey = propertyDefault('horizonVirtKey', false);\n\n  /// general wake lock\n  late final generalWakeLock = propertyDefault('generalWakeLock', false);\n\n  /// ssh page\n  late final sshWakeLock = propertyDefault('sshWakeLock', true);\n  late final sshBgImage = propertyDefault('sshBgImage', '');\n  late final sshBgOpacity = propertyDefault('sshBgOpacity', 0.3);\n  late final sshBlurRadius = propertyDefault('sshBlurRadius', 0.0);\n\n  /// fmt: https://example.com/{DIST}-{BRIGHT}.png\n  late final serverLogoUrl = propertyDefault('serverLogoUrl', '');\n\n  late final betaTest = propertyDefault('betaTest', false);\n\n  /// For desktop only.\n  /// Record the position and size of the window.\n  late final windowState = property<WindowState>(\n    'windowState',\n    fromObj: (raw) => WindowState.fromJson(jsonDecode(raw as String) as Map<String, dynamic>),\n    toObj: (state) => state == null ? null : jsonEncode(state.toJson()),\n  );\n\n  late final introVer = propertyDefault('introVer', 0);\n\n  late final letterCache = propertyDefault('letterCache', false);\n\n  /// Set it to `$EDITOR`, `vim` and etc. to use remote system editor in SSH terminal.\n  /// Set it empty to use local editor GUI.\n  late final sftpEditor = propertyDefault('sftpEditor', '');\n\n  /// Preferred terminal emulator command on desktop\n  late final desktopTerminal = propertyDefault('desktopTerminal', 'x-terminal-emulator');\n\n  /// Run foreground service on Android, if the SSH terminal is running\n  late final fgService = propertyDefault('fgService', false);\n\n  /// Close the editor after saving\n  late final closeAfterSave = propertyDefault('closeAfterSave', false);\n\n  /// Version of store db\n  late final storeVersion = propertyDefault('storeVersion', 0);\n\n  /// Have notified user for notificaiton permission or not\n  late final noNotiPerm = propertyDefault('noNotiPerm', false);\n\n  /// The backup password\n  late final backupasswd = SecureProp('bakPasswd');\n\n  /// Whether to read SSH config from ~/.ssh/config on first time\n  late final firstTimeReadSSHCfg = propertyDefault('firstTimeReadSSHCfg', true);\n\n  /// Tabs at home page\n  late final homeTabs = listProperty(\n    'homeTabs',\n    defaultValue: AppTab.values,\n    fromObj: AppTab.parseAppTabsFromObj,\n    toObj: (val) {\n      return val?.map((e) => e.name).toList() ?? [];\n    },\n  );\n}\n"
  },
  {
    "path": "lib/data/store/snippet.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\n\nimport 'package:server_box/data/model/server/snippet.dart';\n\nclass SnippetStore extends HiveStore {\n  SnippetStore._() : super('snippet');\n\n  static final instance = SnippetStore._();\n\n  void put(Snippet snippet) {\n    set(snippet.name, snippet);\n  }\n\n  List<Snippet> fetch() {\n    final ss = <Snippet>{};\n    for (final key in keys()) {\n      final s = get<Snippet>(\n        key,\n        fromObj: (val) {\n          if (val is Snippet) return val;\n          if (val is Map<dynamic, dynamic>) {\n            final map = val.toStrDynMap;\n            if (map == null) return null;\n            try {\n              final snippet = Snippet.fromJson(map as Map<String, dynamic>);\n              put(snippet);\n              return snippet;\n            } catch (e) {\n              dprint('Parsing Snippet from JSON', e);\n            }\n          }\n          return null;\n        },\n      );\n      if (s != null) {\n        ss.add(s);\n      }\n    }\n    return ss.toList();\n  }\n\n  void delete(Snippet s) {\n    remove(s.name);\n  }\n\n  void update(Snippet old, Snippet newInfo) {\n    if (!have(old)) {\n      throw Exception('Old snippet: $old not found');\n    }\n    delete(old);\n    put(newInfo);\n  }\n\n  bool have(Snippet s) => get(s.name) != null;\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n.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 'l10n_de.dart';\nimport 'l10n_en.dart';\nimport 'l10n_es.dart';\nimport 'l10n_fr.dart';\nimport 'l10n_id.dart';\nimport 'l10n_it.dart';\nimport 'l10n_ja.dart';\nimport 'l10n_ko.dart';\nimport 'l10n_nl.dart';\nimport 'l10n_pt.dart';\nimport 'l10n_ru.dart';\nimport 'l10n_tr.dart';\nimport 'l10n_uk.dart';\nimport 'l10n_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/l10n.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)\n    : 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 =\n      _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 =\n      <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('de'),\n    Locale('en'),\n    Locale('es'),\n    Locale('fr'),\n    Locale('id'),\n    Locale('it'),\n    Locale('ja'),\n    Locale('ko'),\n    Locale('nl'),\n    Locale('pt'),\n    Locale('ru'),\n    Locale('tr'),\n    Locale('uk'),\n    Locale('zh'),\n    Locale('zh', 'TW'),\n  ];\n\n  /// No description provided for @aboutThanks.\n  ///\n  /// In en, this message translates to:\n  /// **'Thanks to the following people who participated in.'**\n  String get aboutThanks;\n\n  /// No description provided for @acceptBeta.\n  ///\n  /// In en, this message translates to:\n  /// **'Accept beta version updates'**\n  String get acceptBeta;\n\n  /// No description provided for @addSystemPrivateKeyTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Currently private keys don\\'t exist, do you want to add the one that comes with the system (~/.ssh/id_rsa)?'**\n  String get addSystemPrivateKeyTip;\n\n  /// No description provided for @added2List.\n  ///\n  /// In en, this message translates to:\n  /// **'Added to task list'**\n  String get added2List;\n\n  /// No description provided for @addr.\n  ///\n  /// In en, this message translates to:\n  /// **'Address'**\n  String get addr;\n\n  /// No description provided for @alreadyLastDir.\n  ///\n  /// In en, this message translates to:\n  /// **'Already in last directory.'**\n  String get alreadyLastDir;\n\n  /// No description provided for @askAi.\n  ///\n  /// In en, this message translates to:\n  /// **'Ask AI'**\n  String get askAi;\n\n  /// No description provided for @askAiApiKey.\n  ///\n  /// In en, this message translates to:\n  /// **'API Key'**\n  String get askAiApiKey;\n\n  /// No description provided for @askAiAwaitingResponse.\n  ///\n  /// In en, this message translates to:\n  /// **'Waiting for AI response...'**\n  String get askAiAwaitingResponse;\n\n  /// No description provided for @askAiBaseUrl.\n  ///\n  /// In en, this message translates to:\n  /// **'Base URL'**\n  String get askAiBaseUrl;\n\n  /// No description provided for @askAiCommandInserted.\n  ///\n  /// In en, this message translates to:\n  /// **'Command inserted into terminal'**\n  String get askAiCommandInserted;\n\n  /// No description provided for @askAiConfigMissing.\n  ///\n  /// In en, this message translates to:\n  /// **'Please configure {fields} in Settings.'**\n  String askAiConfigMissing(Object fields);\n\n  /// No description provided for @askAiConfirmExecute.\n  ///\n  /// In en, this message translates to:\n  /// **'Confirm before executing'**\n  String get askAiConfirmExecute;\n\n  /// No description provided for @askAiConversation.\n  ///\n  /// In en, this message translates to:\n  /// **'AI conversation'**\n  String get askAiConversation;\n\n  /// No description provided for @askAiDisclaimer.\n  ///\n  /// In en, this message translates to:\n  /// **'AI may be incorrect. Review carefully before applying.'**\n  String get askAiDisclaimer;\n\n  /// No description provided for @askAiFollowUpHint.\n  ///\n  /// In en, this message translates to:\n  /// **'Ask a follow-up...'**\n  String get askAiFollowUpHint;\n\n  /// No description provided for @askAiInsertTerminal.\n  ///\n  /// In en, this message translates to:\n  /// **'Insert into terminal'**\n  String get askAiInsertTerminal;\n\n  /// No description provided for @askAiNoResponse.\n  ///\n  /// In en, this message translates to:\n  /// **'No response'**\n  String get askAiNoResponse;\n\n  /// No description provided for @askAiRecommendedCommand.\n  ///\n  /// In en, this message translates to:\n  /// **'AI suggested command'**\n  String get askAiRecommendedCommand;\n\n  /// No description provided for @askAiSelectedContent.\n  ///\n  /// In en, this message translates to:\n  /// **'Selected content'**\n  String get askAiSelectedContent;\n\n  /// No description provided for @askAiUsageHint.\n  ///\n  /// In en, this message translates to:\n  /// **'Used in SSH Terminal'**\n  String get askAiUsageHint;\n\n  /// No description provided for @atLeastOneTab.\n  ///\n  /// In en, this message translates to:\n  /// **'At least one tab must be selected'**\n  String get atLeastOneTab;\n\n  /// No description provided for @authFailTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Authentication failed, please check whether credentials are correct'**\n  String get authFailTip;\n\n  /// No description provided for @autoBackupConflict.\n  ///\n  /// In en, this message translates to:\n  /// **'Only one automatic backup can be turned on at the same time.'**\n  String get autoBackupConflict;\n\n  /// No description provided for @autoConnect.\n  ///\n  /// In en, this message translates to:\n  /// **'Auto connect'**\n  String get autoConnect;\n\n  /// No description provided for @autoRun.\n  ///\n  /// In en, this message translates to:\n  /// **'Auto run'**\n  String get autoRun;\n\n  /// No description provided for @autoUpdateHomeWidget.\n  ///\n  /// In en, this message translates to:\n  /// **'Automatic home widget update'**\n  String get autoUpdateHomeWidget;\n\n  /// No description provided for @availableTabs.\n  ///\n  /// In en, this message translates to:\n  /// **'Available Tabs'**\n  String get availableTabs;\n\n  /// No description provided for @backupEncrypted.\n  ///\n  /// In en, this message translates to:\n  /// **'Backup is encrypted'**\n  String get backupEncrypted;\n\n  /// No description provided for @backupNotEncrypted.\n  ///\n  /// In en, this message translates to:\n  /// **'Backup is not encrypted'**\n  String get backupNotEncrypted;\n\n  /// No description provided for @backupPassword.\n  ///\n  /// In en, this message translates to:\n  /// **'Backup password'**\n  String get backupPassword;\n\n  /// No description provided for @backupPasswordRemoved.\n  ///\n  /// In en, this message translates to:\n  /// **'Backup password removed'**\n  String get backupPasswordRemoved;\n\n  /// No description provided for @backupPasswordSet.\n  ///\n  /// In en, this message translates to:\n  /// **'Backup password set'**\n  String get backupPasswordSet;\n\n  /// No description provided for @backupPasswordTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Set a password to encrypt backup files. Leave empty to disable encryption.'**\n  String get backupPasswordTip;\n\n  /// No description provided for @backupPasswordWrong.\n  ///\n  /// In en, this message translates to:\n  /// **'Incorrect backup password'**\n  String get backupPasswordWrong;\n\n  /// No description provided for @backupTip.\n  ///\n  /// In en, this message translates to:\n  /// **'The exported data can be encrypted with password. \\nPlease keep it safe.'**\n  String get backupTip;\n\n  /// No description provided for @backupVersionNotMatch.\n  ///\n  /// In en, this message translates to:\n  /// **'Backup version is not match.'**\n  String get backupVersionNotMatch;\n\n  /// No description provided for @bgRun.\n  ///\n  /// In en, this message translates to:\n  /// **'Run in background'**\n  String get bgRun;\n\n  /// No description provided for @bgRunTip.\n  ///\n  /// In en, this message translates to:\n  /// **'This switch only means the program will try to run in the background. Whether it can run in the background depends on whether the permission is enabled or not. For AOSP-based Android ROMs, please disable \\\"Battery Optimization\\\" in this app. For MIUI / HyperOS, please change the power saving policy to \\\"Unlimited\\\".'**\n  String get bgRunTip;\n\n  /// No description provided for @clearAllStatsContent.\n  ///\n  /// In en, this message translates to:\n  /// **'Are you sure you want to clear all server connection statistics? This action cannot be undone.'**\n  String get clearAllStatsContent;\n\n  /// No description provided for @clearAllStatsTitle.\n  ///\n  /// In en, this message translates to:\n  /// **'Clear All Statistics'**\n  String get clearAllStatsTitle;\n\n  /// No description provided for @clearServerStatsContent.\n  ///\n  /// In en, this message translates to:\n  /// **'Are you sure you want to clear connection statistics for server \\\"{serverName}\\\"? This action cannot be undone.'**\n  String clearServerStatsContent(Object serverName);\n\n  /// No description provided for @clearServerStatsTitle.\n  ///\n  /// In en, this message translates to:\n  /// **'Clear {serverName} Statistics'**\n  String clearServerStatsTitle(Object serverName);\n\n  /// No description provided for @clearThisServerStats.\n  ///\n  /// In en, this message translates to:\n  /// **'Clear This Server Statistics'**\n  String get clearThisServerStats;\n\n  /// No description provided for @compactDatabase.\n  ///\n  /// In en, this message translates to:\n  /// **'Compact Database'**\n  String get compactDatabase;\n\n  /// No description provided for @compactDatabaseContent.\n  ///\n  /// In en, this message translates to:\n  /// **'Database size: {size}\\n\\nThis will reorganize the database to reduce file size. No data will be deleted.'**\n  String compactDatabaseContent(Object size);\n\n  /// No description provided for @closeAfterSave.\n  ///\n  /// In en, this message translates to:\n  /// **'Save and close'**\n  String get closeAfterSave;\n\n  /// No description provided for @collapseUITip.\n  ///\n  /// In en, this message translates to:\n  /// **'Whether to collapse long lists present in the UI by default'**\n  String get collapseUITip;\n\n  /// No description provided for @connectionDetails.\n  ///\n  /// In en, this message translates to:\n  /// **'Connection Details'**\n  String get connectionDetails;\n\n  /// No description provided for @connectionStats.\n  ///\n  /// In en, this message translates to:\n  /// **'Connection Statistics'**\n  String get connectionStats;\n\n  /// No description provided for @connectionStatsDesc.\n  ///\n  /// In en, this message translates to:\n  /// **'View server connection success rate and history'**\n  String get connectionStatsDesc;\n\n  /// No description provided for @containerTrySudoTip.\n  ///\n  /// In en, this message translates to:\n  /// **'For example: In the app, the user is set to aaa, but Docker is installed under the root user. In this case, you need to enable this option.'**\n  String get containerTrySudoTip;\n\n  /// No description provided for @containerSudoPasswordRequired.\n  ///\n  /// In en, this message translates to:\n  /// **'Sudo password is required to access Docker. Please enter your password.'**\n  String get containerSudoPasswordRequired;\n\n  /// No description provided for @containerSudoPasswordIncorrect.\n  ///\n  /// In en, this message translates to:\n  /// **'Sudo password is incorrect or not allowed. Please try again.'**\n  String get containerSudoPasswordIncorrect;\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 @copyPath.\n  ///\n  /// In en, this message translates to:\n  /// **'Copy path'**\n  String get copyPath;\n\n  /// No description provided for @cpuViewAsProgressTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Display the usage of each CPU in a progress bar style (old style)'**\n  String get cpuViewAsProgressTip;\n\n  /// No description provided for @cursorType.\n  ///\n  /// In en, this message translates to:\n  /// **'Cursor type'**\n  String get cursorType;\n\n  /// No description provided for @customCmd.\n  ///\n  /// In en, this message translates to:\n  /// **'Custom commands'**\n  String get customCmd;\n\n  /// No description provided for @customCmdHint.\n  ///\n  /// In en, this message translates to:\n  /// **'\\\"Command Name\\\": \\\"Command\\\"'**\n  String get customCmdHint;\n\n  /// No description provided for @deleteServers.\n  ///\n  /// In en, this message translates to:\n  /// **'Batch delete servers'**\n  String get deleteServers;\n\n  /// No description provided for @desktopTerminalTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Command used to open the terminal emulator when launching SSH sessions.'**\n  String get desktopTerminalTip;\n\n  /// No description provided for @dirEmpty.\n  ///\n  /// In en, this message translates to:\n  /// **'Make sure the folder is empty.'**\n  String get dirEmpty;\n\n  /// No description provided for @discoverSshServers.\n  ///\n  /// In en, this message translates to:\n  /// **'Discover SSH Servers'**\n  String get discoverSshServers;\n\n  /// No description provided for @discoveryFailed.\n  ///\n  /// In en, this message translates to:\n  /// **'Discovery failed'**\n  String get discoveryFailed;\n\n  /// No description provided for @discoverySettings.\n  ///\n  /// In en, this message translates to:\n  /// **'Discovery Settings'**\n  String get discoverySettings;\n\n  /// No description provided for @discoverySummary.\n  ///\n  /// In en, this message translates to:\n  /// **'Discovery Summary'**\n  String get discoverySummary;\n\n  /// No description provided for @diskHealth.\n  ///\n  /// In en, this message translates to:\n  /// **'Disk Health'**\n  String get diskHealth;\n\n  /// No description provided for @diskIgnorePath.\n  ///\n  /// In en, this message translates to:\n  /// **'Ignore path for disk'**\n  String get diskIgnorePath;\n\n  /// No description provided for @displayCpuIndex.\n  ///\n  /// In en, this message translates to:\n  /// **'Display CPU index'**\n  String get displayCpuIndex;\n\n  /// No description provided for @dl2Local.\n  ///\n  /// In en, this message translates to:\n  /// **'Download {fileName} to local?'**\n  String dl2Local(Object fileName);\n\n  /// No description provided for @dockerEmptyRunningItems.\n  ///\n  /// In en, this message translates to:\n  /// **'There are no running containers.\\nThis could be because:\\n- The Docker installation user is not the same as the username configured within the App.\\n- The environment variable DOCKER_HOST was not read correctly. You can get it by running `echo \\$DOCKER_HOST` in the terminal.'**\n  String get dockerEmptyRunningItems;\n\n  /// No description provided for @dockerImagesFmt.\n  ///\n  /// In en, this message translates to:\n  /// **'{count} images'**\n  String dockerImagesFmt(Object count);\n\n  /// No description provided for @dockerNotInstalled.\n  ///\n  /// In en, this message translates to:\n  /// **'Docker not installed'**\n  String get dockerNotInstalled;\n\n  /// No description provided for @dockerStatusRunningAndStoppedFmt.\n  ///\n  /// In en, this message translates to:\n  /// **'{runningCount} running, {stoppedCount} container stopped.'**\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  );\n\n  /// No description provided for @dockerStatusRunningFmt.\n  ///\n  /// In en, this message translates to:\n  /// **'{count} container running.'**\n  String dockerStatusRunningFmt(Object count);\n\n  /// No description provided for @doubleColumnMode.\n  ///\n  /// In en, this message translates to:\n  /// **'Double column mode'**\n  String get doubleColumnMode;\n\n  /// No description provided for @doubleColumnTip.\n  ///\n  /// In en, this message translates to:\n  /// **'This option only enables the feature, whether it can actually be enabled depends on the width of the device'**\n  String get doubleColumnTip;\n\n  /// No description provided for @editVirtKeys.\n  ///\n  /// In en, this message translates to:\n  /// **'Edit virtual keys'**\n  String get editVirtKeys;\n\n  /// No description provided for @editorHighlightTip.\n  ///\n  /// In en, this message translates to:\n  /// **'The current code highlighting performance is not ideal and can be optionally turned off to improve.'**\n  String get editorHighlightTip;\n\n  /// No description provided for @enableMdns.\n  ///\n  /// In en, this message translates to:\n  /// **'Enable mDNS'**\n  String get enableMdns;\n\n  /// No description provided for @enableMdnsDesc.\n  ///\n  /// In en, this message translates to:\n  /// **'Use mDNS/Bonjour to discover SSH services'**\n  String get enableMdnsDesc;\n\n  /// No description provided for @envVars.\n  ///\n  /// In en, this message translates to:\n  /// **'Environment variable'**\n  String get envVars;\n\n  /// No description provided for @experimentalFeature.\n  ///\n  /// In en, this message translates to:\n  /// **'Experimental feature'**\n  String get experimentalFeature;\n\n  /// No description provided for @extraArgs.\n  ///\n  /// In en, this message translates to:\n  /// **'Extra arguments'**\n  String get extraArgs;\n\n  /// No description provided for @fallbackSshDest.\n  ///\n  /// In en, this message translates to:\n  /// **'Fallback SSH destination'**\n  String get fallbackSshDest;\n\n  /// No description provided for @fdroidReleaseTip.\n  ///\n  /// In en, this message translates to:\n  /// **'If you downloaded this app from F-Droid, it is recommended to turn off this option.'**\n  String get fdroidReleaseTip;\n\n  /// No description provided for @fgService.\n  ///\n  /// In en, this message translates to:\n  /// **'Foreground Service'**\n  String get fgService;\n\n  /// No description provided for @fgServiceTip.\n  ///\n  /// In en, this message translates to:\n  /// **'After enabling, some device models may crash. Disabling it may cause some models to be unable to maintain SSH connections in the background. Please allow ServerBox notification permissions, background running, and self-wake-up in system settings.'**\n  String get fgServiceTip;\n\n  /// No description provided for @fileTooLarge.\n  ///\n  /// In en, this message translates to:\n  /// **'File \\'{file}\\' too large {size}, max {sizeMax}'**\n  String fileTooLarge(Object file, Object size, Object sizeMax);\n\n  /// No description provided for @finishedAt.\n  ///\n  /// In en, this message translates to:\n  /// **'Finished at'**\n  String get finishedAt;\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 @fontSize.\n  ///\n  /// In en, this message translates to:\n  /// **'Font size'**\n  String get fontSize;\n\n  /// No description provided for @fullScreen.\n  ///\n  /// In en, this message translates to:\n  /// **'Full screen mode'**\n  String get fullScreen;\n\n  /// No description provided for @fullScreenJitter.\n  ///\n  /// In en, this message translates to:\n  /// **'Full screen jitter'**\n  String get fullScreenJitter;\n\n  /// No description provided for @fullScreenJitterHelp.\n  ///\n  /// In en, this message translates to:\n  /// **'To avoid screen burn-in'**\n  String get fullScreenJitterHelp;\n\n  /// No description provided for @fullScreenTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Should full-screen mode be enabled when the device is rotated to landscape mode? This option only applies to the server tab.'**\n  String get fullScreenTip;\n\n  /// No description provided for @goBackQ.\n  ///\n  /// In en, this message translates to:\n  /// **'Go back?'**\n  String get goBackQ;\n\n  /// No description provided for @goto.\n  ///\n  /// In en, this message translates to:\n  /// **'Go to'**\n  String get goto;\n\n  /// No description provided for @hideTitleBar.\n  ///\n  /// In en, this message translates to:\n  /// **'Hide title bar'**\n  String get hideTitleBar;\n\n  /// No description provided for @highlight.\n  ///\n  /// In en, this message translates to:\n  /// **'Code highlighting'**\n  String get highlight;\n\n  /// No description provided for @homeTabs.\n  ///\n  /// In en, this message translates to:\n  /// **'Home Tabs'**\n  String get homeTabs;\n\n  /// No description provided for @homeTabsCustomizeDesc.\n  ///\n  /// In en, this message translates to:\n  /// **'Customize which tabs appear on the home page and their order'**\n  String get homeTabsCustomizeDesc;\n\n  /// No description provided for @homeWidgetUrlConfig.\n  ///\n  /// In en, this message translates to:\n  /// **'Config home widget url'**\n  String get homeWidgetUrlConfig;\n\n  /// No description provided for @httpFailedWithCode.\n  ///\n  /// In en, this message translates to:\n  /// **'request failed, status code: {code}'**\n  String httpFailedWithCode(Object code);\n\n  /// No description provided for @ignoreCert.\n  ///\n  /// In en, this message translates to:\n  /// **'Ignore certificate'**\n  String get ignoreCert;\n\n  /// No description provided for @image.\n  ///\n  /// In en, this message translates to:\n  /// **'Image'**\n  String get image;\n\n  /// No description provided for @imagesList.\n  ///\n  /// In en, this message translates to:\n  /// **'Images list'**\n  String get imagesList;\n\n  /// No description provided for @installDockerWithUrl.\n  ///\n  /// In en, this message translates to:\n  /// **'Please https://docs.docker.com/engine/install docker first.'**\n  String get installDockerWithUrl;\n\n  /// No description provided for @invalid.\n  ///\n  /// In en, this message translates to:\n  /// **'Invalid'**\n  String get invalid;\n\n  /// No description provided for @invalidHostFormat.\n  ///\n  /// In en, this message translates to:\n  /// **'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.'**\n  String get invalidHostFormat;\n\n  /// No description provided for @jumpServer.\n  ///\n  /// In en, this message translates to:\n  /// **'Jump server'**\n  String get jumpServer;\n\n  /// No description provided for @keepForeground.\n  ///\n  /// In en, this message translates to:\n  /// **'Keep app foreground!'**\n  String get keepForeground;\n\n  /// No description provided for @keepStatusWhenErr.\n  ///\n  /// In en, this message translates to:\n  /// **'Preserve the last server state'**\n  String get keepStatusWhenErr;\n\n  /// No description provided for @keepStatusWhenErrTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Only in the event of an error during script execution'**\n  String get keepStatusWhenErrTip;\n\n  /// No description provided for @keyAuth.\n  ///\n  /// In en, this message translates to:\n  /// **'Key Auth'**\n  String get keyAuth;\n\n  /// No description provided for @lastFailure.\n  ///\n  /// In en, this message translates to:\n  /// **'Last Failure'**\n  String get lastFailure;\n\n  /// No description provided for @lastSuccess.\n  ///\n  /// In en, this message translates to:\n  /// **'Last Success'**\n  String get lastSuccess;\n\n  /// No description provided for @letterCache.\n  ///\n  /// In en, this message translates to:\n  /// **'Letter caching'**\n  String get letterCache;\n\n  /// No description provided for @letterCacheTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Recommended to disable, but after disabling, it will be impossible to input CJK characters.'**\n  String get letterCacheTip;\n\n  /// No description provided for @madeWithLove.\n  ///\n  /// In en, this message translates to:\n  /// **'Made with ❤️ by {myGithub}'**\n  String madeWithLove(Object myGithub);\n\n  /// No description provided for @max.\n  ///\n  /// In en, this message translates to:\n  /// **'max'**\n  String get max;\n\n  /// No description provided for @maxConcurrency.\n  ///\n  /// In en, this message translates to:\n  /// **'Max Concurrency'**\n  String get maxConcurrency;\n\n  /// No description provided for @maxRetryCount.\n  ///\n  /// In en, this message translates to:\n  /// **'Number of server reconnections'**\n  String get maxRetryCount;\n\n  /// No description provided for @maxRetryCountEqual0.\n  ///\n  /// In en, this message translates to:\n  /// **'Will retry again and again.'**\n  String get maxRetryCountEqual0;\n\n  /// No description provided for @min.\n  ///\n  /// In en, this message translates to:\n  /// **'min'**\n  String get min;\n\n  /// No description provided for @more.\n  ///\n  /// In en, this message translates to:\n  /// **'More'**\n  String get more;\n\n  /// No description provided for @moveOutServerFuncBtnsHelp.\n  ///\n  /// In en, this message translates to:\n  /// **'On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.'**\n  String get moveOutServerFuncBtnsHelp;\n\n  /// No description provided for @needHomeDir.\n  ///\n  /// In en, this message translates to:\n  /// **'If you are a Synology user, [see here](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Users of other systems need to search for how to create a home directory.'**\n  String get needHomeDir;\n\n  /// No description provided for @needRestart.\n  ///\n  /// In en, this message translates to:\n  /// **'App needs to be restarted'**\n  String get needRestart;\n\n  /// No description provided for @netViewType.\n  ///\n  /// In en, this message translates to:\n  /// **'Network view type'**\n  String get netViewType;\n\n  /// No description provided for @newContainer.\n  ///\n  /// In en, this message translates to:\n  /// **'New container'**\n  String get newContainer;\n\n  /// No description provided for @noConnectionStatsData.\n  ///\n  /// In en, this message translates to:\n  /// **'No connection statistics data'**\n  String get noConnectionStatsData;\n\n  /// No description provided for @noLineChart.\n  ///\n  /// In en, this message translates to:\n  /// **'Do not use line charts'**\n  String get noLineChart;\n\n  /// No description provided for @noLineChartForCpu.\n  ///\n  /// In en, this message translates to:\n  /// **'Do not use line charts for CPU'**\n  String get noLineChartForCpu;\n\n  /// No description provided for @noPrivateKeyTip.\n  ///\n  /// In en, this message translates to:\n  /// **'The private key does not exist, it may have been deleted or there is a configuration error.'**\n  String get noPrivateKeyTip;\n\n  /// No description provided for @noPromptAgain.\n  ///\n  /// In en, this message translates to:\n  /// **'Do not prompt again'**\n  String get noPromptAgain;\n\n  /// No description provided for @onServerDetailPage.\n  ///\n  /// In en, this message translates to:\n  /// **'On server detail page'**\n  String get onServerDetailPage;\n\n  /// No description provided for @onlyOneLine.\n  ///\n  /// In en, this message translates to:\n  /// **'Only display as one line (scrollable)'**\n  String get onlyOneLine;\n\n  /// No description provided for @onlyWhenCoreBiggerThan8.\n  ///\n  /// In en, this message translates to:\n  /// **'Works only when the number of cores is greater than 8'**\n  String get onlyWhenCoreBiggerThan8;\n\n  /// No description provided for @openLastPath.\n  ///\n  /// In en, this message translates to:\n  /// **'Open the last path'**\n  String get openLastPath;\n\n  /// No description provided for @openLastPathTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Different servers will have different logs, and the log is the path to the exit'**\n  String get openLastPathTip;\n\n  /// No description provided for @parseContainerStatsTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Parsing the occupancy status of Docker is relatively slow.'**\n  String get parseContainerStatsTip;\n\n  /// No description provided for @percentOfSize.\n  ///\n  /// In en, this message translates to:\n  /// **'{percent}% of {size}'**\n  String percentOfSize(Object percent, Object size);\n\n  /// No description provided for @permission.\n  ///\n  /// In en, this message translates to:\n  /// **'Permissions'**\n  String get permission;\n\n  /// No description provided for @pingInputIP.\n  ///\n  /// In en, this message translates to:\n  /// **'Please input a target IP / domain.'**\n  String get pingInputIP;\n\n  /// No description provided for @pingNoServer.\n  ///\n  /// In en, this message translates to:\n  /// **'No server to ping.\\nPlease add a server in server tab.'**\n  String get pingNoServer;\n\n  /// No description provided for @plugInType.\n  ///\n  /// In en, this message translates to:\n  /// **'Insertion Type'**\n  String get plugInType;\n\n  /// No description provided for @preferDiskAmount.\n  ///\n  /// In en, this message translates to:\n  /// **'Prioritize displaying disk capacity'**\n  String get preferDiskAmount;\n\n  /// No description provided for @privateKey.\n  ///\n  /// In en, this message translates to:\n  /// **'Private Key'**\n  String get privateKey;\n\n  /// No description provided for @privateKeyNotFoundFmt.\n  ///\n  /// In en, this message translates to:\n  /// **'Private key [{keyId}] not found.'**\n  String privateKeyNotFoundFmt(Object keyId);\n\n  /// No description provided for @pushToken.\n  ///\n  /// In en, this message translates to:\n  /// **'Push token'**\n  String get pushToken;\n\n  /// No description provided for @pveIgnoreCertTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Not recommended to enable, beware of security risks! If you are using the default certificate from PVE, you need to enable this option.'**\n  String get pveIgnoreCertTip;\n\n  /// No description provided for @pveLoginFailed.\n  ///\n  /// In en, this message translates to:\n  /// **'Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.'**\n  String get pveLoginFailed;\n\n  /// No description provided for @pveVersionLow.\n  ///\n  /// In en, this message translates to:\n  /// **'This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.'**\n  String get pveVersionLow;\n\n  /// No description provided for @read.\n  ///\n  /// In en, this message translates to:\n  /// **'Read'**\n  String get read;\n\n  /// No description provided for @recentConnections.\n  ///\n  /// In en, this message translates to:\n  /// **'Recent Connections'**\n  String get recentConnections;\n\n  /// No description provided for @rememberPwdInMem.\n  ///\n  /// In en, this message translates to:\n  /// **'Remember password in memory'**\n  String get rememberPwdInMem;\n\n  /// No description provided for @rememberPwdInMemTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Used for containers, suspending, etc.'**\n  String get rememberPwdInMemTip;\n\n  /// No description provided for @rememberWindowSize.\n  ///\n  /// In en, this message translates to:\n  /// **'Remember window size'**\n  String get rememberWindowSize;\n\n  /// No description provided for @remotePath.\n  ///\n  /// In en, this message translates to:\n  /// **'Remote path'**\n  String get remotePath;\n\n  /// No description provided for @result.\n  ///\n  /// In en, this message translates to:\n  /// **'Result'**\n  String get result;\n\n  /// No description provided for @rotateAngel.\n  ///\n  /// In en, this message translates to:\n  /// **'Rotation angle'**\n  String get rotateAngel;\n\n  /// No description provided for @sameIdServerExist.\n  ///\n  /// In en, this message translates to:\n  /// **'A server with the same ID already exists'**\n  String get sameIdServerExist;\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 @second.\n  ///\n  /// In en, this message translates to:\n  /// **'s'**\n  String get second;\n\n  /// No description provided for @serverDetailOrder.\n  ///\n  /// In en, this message translates to:\n  /// **'Detail page widget order'**\n  String get serverDetailOrder;\n\n  /// No description provided for @serverFuncBtns.\n  ///\n  /// In en, this message translates to:\n  /// **'Server function buttons'**\n  String get serverFuncBtns;\n\n  /// No description provided for @serverOrder.\n  ///\n  /// In en, this message translates to:\n  /// **'Server order'**\n  String get serverOrder;\n\n  /// No description provided for @serverTabRequired.\n  ///\n  /// In en, this message translates to:\n  /// **'Server tab cannot be removed'**\n  String get serverTabRequired;\n\n  /// No description provided for @sftpDlPrepare.\n  ///\n  /// In en, this message translates to:\n  /// **'Preparing to connect...'**\n  String get sftpDlPrepare;\n\n  /// No description provided for @sftpEditorTip.\n  ///\n  /// In en, this message translates to:\n  /// **'If empty, use the built-in file editor of the app. If a value is present, use the remote server’s editor, e.g., `vim` (recommended to automatically detect according to `EDITOR`).'**\n  String get sftpEditorTip;\n\n  /// No description provided for @sftpRmrDirSummary.\n  ///\n  /// In en, this message translates to:\n  /// **'Use `rm -r` to delete a folder in SFTP.'**\n  String get sftpRmrDirSummary;\n\n  /// No description provided for @sftpSSHConnected.\n  ///\n  /// In en, this message translates to:\n  /// **'SFTP Connected'**\n  String get sftpSSHConnected;\n\n  /// No description provided for @sftpShowFoldersFirst.\n  ///\n  /// In en, this message translates to:\n  /// **'Display folders first'**\n  String get sftpShowFoldersFirst;\n\n  /// No description provided for @showDistLogo.\n  ///\n  /// In en, this message translates to:\n  /// **'Show distribution logo'**\n  String get showDistLogo;\n\n  /// No description provided for @size.\n  ///\n  /// In en, this message translates to:\n  /// **'Size'**\n  String get size;\n\n  /// No description provided for @softWrap.\n  ///\n  /// In en, this message translates to:\n  /// **'Soft wrap'**\n  String get softWrap;\n\n  /// No description provided for @specifyDev.\n  ///\n  /// In en, this message translates to:\n  /// **'Specify device'**\n  String get specifyDev;\n\n  /// No description provided for @specifyDevTip.\n  ///\n  /// In en, this message translates to:\n  /// **'For example, network traffic statistics are by default for all devices. You can specify a particular device here.'**\n  String get specifyDevTip;\n\n  /// No description provided for @speed.\n  ///\n  /// In en, this message translates to:\n  /// **'Speed'**\n  String get speed;\n\n  /// No description provided for @spentTime.\n  ///\n  /// In en, this message translates to:\n  /// **'Spent time: {time}'**\n  String spentTime(Object time);\n\n  /// No description provided for @sshConfigAllExist.\n  ///\n  /// In en, this message translates to:\n  /// **'All servers already exist ({duplicateCount} duplicates found)'**\n  String sshConfigAllExist(Object duplicateCount);\n\n  /// No description provided for @sshConfigDuplicatesSkipped.\n  ///\n  /// In en, this message translates to:\n  /// **'{duplicateCount} duplicates will be skipped'**\n  String sshConfigDuplicatesSkipped(Object duplicateCount);\n\n  /// No description provided for @sshConfigFound.\n  ///\n  /// In en, this message translates to:\n  /// **'We found SSH configuration on your system.'**\n  String get sshConfigFound;\n\n  /// No description provided for @sshConfigFoundServers.\n  ///\n  /// In en, this message translates to:\n  /// **'Found {totalCount} servers'**\n  String sshConfigFoundServers(Object totalCount);\n\n  /// No description provided for @sshConfigImport.\n  ///\n  /// In en, this message translates to:\n  /// **'SSH Config Import'**\n  String get sshConfigImport;\n\n  /// No description provided for @sshConfigImportHelp.\n  ///\n  /// In en, this message translates to:\n  /// **'Only basic information can be imported, for example: IP/Port.'**\n  String get sshConfigImportHelp;\n\n  /// No description provided for @sshConfigImportPermission.\n  ///\n  /// In en, this message translates to:\n  /// **'Would you like to give permission to read ~/.ssh/config and automatically import server settings?'**\n  String get sshConfigImportPermission;\n\n  /// No description provided for @sshConfigImportTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Prompt to read ~/.ssh/config on first server creation'**\n  String get sshConfigImportTip;\n\n  /// No description provided for @sshConfigImported.\n  ///\n  /// In en, this message translates to:\n  /// **'Imported {count} servers from SSH config'**\n  String sshConfigImported(Object count);\n\n  /// No description provided for @sshHostKeyChangedDesc.\n  ///\n  /// In en, this message translates to:\n  /// **'The SSH host key changed for {serverName}. Only continue if you trust this server.'**\n  String sshHostKeyChangedDesc(Object serverName);\n\n  /// No description provided for @sshHostKeyFingerprintMd5Base64.\n  ///\n  /// In en, this message translates to:\n  /// **'Fingerprint (MD5 base64): {fingerprint}'**\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint);\n\n  /// No description provided for @sshHostKeyFingerprintMd5Hex.\n  ///\n  /// In en, this message translates to:\n  /// **'Fingerprint (MD5 hex): {fingerprint}'**\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint);\n\n  /// Label for the SSH host key type displayed in the host key verification dialog.\n  ///\n  /// In en, this message translates to:\n  /// **'SSH host key type'**\n  String get sshHostKeyType;\n\n  /// No description provided for @sshHostKeyNewDesc.\n  ///\n  /// In en, this message translates to:\n  /// **'A new SSH host key was received from {serverName}. Review the fingerprint before trusting.'**\n  String sshHostKeyNewDesc(Object serverName);\n\n  /// No description provided for @sshHostKeyStoredFingerprint.\n  ///\n  /// In en, this message translates to:\n  /// **'Stored fingerprint: {fingerprint}'**\n  String sshHostKeyStoredFingerprint(Object fingerprint);\n\n  /// No description provided for @sshConfigManualSelect.\n  ///\n  /// In en, this message translates to:\n  /// **'Would you like to select the SSH config file manually?'**\n  String get sshConfigManualSelect;\n\n  /// No description provided for @sshConfigNoServers.\n  ///\n  /// In en, this message translates to:\n  /// **'No servers found in SSH config'**\n  String get sshConfigNoServers;\n\n  /// No description provided for @sshConfigPermissionDenied.\n  ///\n  /// In en, this message translates to:\n  /// **'Cannot access SSH config file due to macOS permissions.'**\n  String get sshConfigPermissionDenied;\n\n  /// No description provided for @sshConfigServersToImport.\n  ///\n  /// In en, this message translates to:\n  /// **'{importCount} servers will be imported'**\n  String sshConfigServersToImport(Object importCount);\n\n  /// No description provided for @sshTermHelp.\n  ///\n  /// In en, this message translates to:\n  /// **'When the terminal is scrollable, dragging horizontally can select text. Clicking the keyboard button turns the keyboard on/off. The file icon opens the current path SFTP. The clipboard button copies the content when text is selected, and pastes content from the clipboard into the terminal when no text is selected and there is content on the clipboard. The code icon pastes code snippets into the terminal and executes them.'**\n  String get sshTermHelp;\n\n  /// No description provided for @sshTip.\n  ///\n  /// In en, this message translates to:\n  /// **'This function is now in the experimental stage.\\n\\nPlease report bugs on {url} or join our development.'**\n  String sshTip(Object url);\n\n  /// No description provided for @sshVirtualKeyAutoOff.\n  ///\n  /// In en, this message translates to:\n  /// **'Auto switching of virtual keys'**\n  String get sshVirtualKeyAutoOff;\n\n  /// No description provided for @stat.\n  ///\n  /// In en, this message translates to:\n  /// **'Statistics'**\n  String get stat;\n\n  /// No description provided for @supportFmtArgs.\n  ///\n  /// In en, this message translates to:\n  /// **'The following formatting parameters are supported:'**\n  String get supportFmtArgs;\n\n  /// No description provided for @suspendTip.\n  ///\n  /// In en, this message translates to:\n  /// **'The suspend function requires root permission and systemd support.'**\n  String get suspendTip;\n\n  /// No description provided for @switchTo.\n  ///\n  /// In en, this message translates to:\n  /// **'Switch to {val}'**\n  String switchTo(Object val);\n\n  /// No description provided for @syncTip.\n  ///\n  /// In en, this message translates to:\n  /// **'A restart may be required for some changes to take effect.'**\n  String get syncTip;\n\n  /// No description provided for @system.\n  ///\n  /// In en, this message translates to:\n  /// **'System'**\n  String get system;\n\n  /// No description provided for @tag.\n  ///\n  /// In en, this message translates to:\n  /// **'Tags'**\n  String get tag;\n\n  /// No description provided for @tapToStartDiscovery.\n  ///\n  /// In en, this message translates to:\n  /// **'Tap the search button to discover SSH servers on your network'**\n  String get tapToStartDiscovery;\n\n  /// No description provided for @termFontSizeTip.\n  ///\n  /// In en, this message translates to:\n  /// **'This setting will affect the terminal size (width and height). You can zoom in on the terminal page to adjust the font size of the current session.'**\n  String get termFontSizeTip;\n\n  /// No description provided for @textScaler.\n  ///\n  /// In en, this message translates to:\n  /// **'Text scaler'**\n  String get textScaler;\n\n  /// No description provided for @textScalerTip.\n  ///\n  /// In en, this message translates to:\n  /// **'1.0 => 100% (original size), only works on server page part of the font, not recommended to change.'**\n  String get textScalerTip;\n\n  /// No description provided for @time.\n  ///\n  /// In en, this message translates to:\n  /// **'Time'**\n  String get time;\n\n  /// No description provided for @times.\n  ///\n  /// In en, this message translates to:\n  /// **'Times'**\n  String get times;\n\n  /// No description provided for @trySudo.\n  ///\n  /// In en, this message translates to:\n  /// **'Try using sudo'**\n  String get trySudo;\n\n  /// No description provided for @unknown.\n  ///\n  /// In en, this message translates to:\n  /// **'Unknown'**\n  String get unknown;\n\n  /// No description provided for @unkownConvertMode.\n  ///\n  /// In en, this message translates to:\n  /// **'Unknown conversion mode'**\n  String get unkownConvertMode;\n\n  /// No description provided for @update.\n  ///\n  /// In en, this message translates to:\n  /// **'Update'**\n  String get update;\n\n  /// No description provided for @updateIntervalEqual0.\n  ///\n  /// In en, this message translates to:\n  /// **'You set to 0, will not update automatically.\\nCan\\'t calculate CPU status.'**\n  String get updateIntervalEqual0;\n\n  /// No description provided for @updateServerStatusInterval.\n  ///\n  /// In en, this message translates to:\n  /// **'Server status update interval'**\n  String get updateServerStatusInterval;\n\n  /// No description provided for @upsideDown.\n  ///\n  /// In en, this message translates to:\n  /// **'Upside Down'**\n  String get upsideDown;\n\n  /// No description provided for @useCdn.\n  ///\n  /// In en, this message translates to:\n  /// **'Using CDN'**\n  String get useCdn;\n\n  /// No description provided for @useCdnTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Non-Chinese users are recommended to use CDN. Would you like to use it?'**\n  String get useCdnTip;\n\n  /// No description provided for @useNoPwd.\n  ///\n  /// In en, this message translates to:\n  /// **'No password will be used'**\n  String get useNoPwd;\n\n  /// No description provided for @usePodmanByDefault.\n  ///\n  /// In en, this message translates to:\n  /// **'Use Podman by default'**\n  String get usePodmanByDefault;\n\n  /// No description provided for @used.\n  ///\n  /// In en, this message translates to:\n  /// **'Used'**\n  String get used;\n\n  /// No description provided for @view.\n  ///\n  /// In en, this message translates to:\n  /// **'View'**\n  String get view;\n\n  /// No description provided for @viewDetails.\n  ///\n  /// In en, this message translates to:\n  /// **'View Details'**\n  String get viewDetails;\n\n  /// No description provided for @viewErr.\n  ///\n  /// In en, this message translates to:\n  /// **'See error'**\n  String get viewErr;\n\n  /// No description provided for @virtKeyHelpClipboard.\n  ///\n  /// In en, this message translates to:\n  /// **'Copy to the clipboard if the selected terminal is not empty, otherwise paste the content of the clipboard to the terminal.'**\n  String get virtKeyHelpClipboard;\n\n  /// No description provided for @virtKeyHelpIME.\n  ///\n  /// In en, this message translates to:\n  /// **'Turn on/off the keyboard'**\n  String get virtKeyHelpIME;\n\n  /// No description provided for @virtKeyHelpSFTP.\n  ///\n  /// In en, this message translates to:\n  /// **'Open current directory in SFTP.'**\n  String get virtKeyHelpSFTP;\n\n  /// No description provided for @waitConnection.\n  ///\n  /// In en, this message translates to:\n  /// **'Please wait for the connection to be established.'**\n  String get waitConnection;\n\n  /// No description provided for @wakeLock.\n  ///\n  /// In en, this message translates to:\n  /// **'Keep awake'**\n  String get wakeLock;\n\n  /// No description provided for @watchNotPaired.\n  ///\n  /// In en, this message translates to:\n  /// **'No paired Apple Watch'**\n  String get watchNotPaired;\n\n  /// No description provided for @webdavSettingEmpty.\n  ///\n  /// In en, this message translates to:\n  /// **'WebDav setting is empty'**\n  String get webdavSettingEmpty;\n\n  /// No description provided for @whenOpenApp.\n  ///\n  /// In en, this message translates to:\n  /// **'When opening the app'**\n  String get whenOpenApp;\n\n  /// No description provided for @wolTip.\n  ///\n  /// In en, this message translates to:\n  /// **'After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.'**\n  String get wolTip;\n\n  /// No description provided for @write.\n  ///\n  /// In en, this message translates to:\n  /// **'Write'**\n  String get write;\n\n  /// No description provided for @writeScriptFailTip.\n  ///\n  /// In en, this message translates to:\n  /// **'Writing to the script failed, possibly due to lack of permissions or the directory does not exist.'**\n  String get writeScriptFailTip;\n\n  /// No description provided for @writeScriptTip.\n  ///\n  /// In en, this message translates to:\n  /// **'After connecting to the server, a script will be written to `~/.config/server_box` \\n | `/tmp/server_box` to monitor the system status. You can review the script content.'**\n  String get writeScriptTip;\n\n  /// No description provided for @menuGitHubRepository.\n  ///\n  /// In en, this message translates to:\n  /// **'GitHub Repository'**\n  String get menuGitHubRepository;\n\n  /// No description provided for @podmanDockerEmulationDetected.\n  ///\n  /// In en, this message translates to:\n  /// **'Podman Docker emulation detected. Please switch to Podman in settings.'**\n  String get podmanDockerEmulationDetected;\n}\n\nclass _AppLocalizationsDelegate\n    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>[\n    'de',\n    'en',\n    'es',\n    'fr',\n    'id',\n    'it',\n    'ja',\n    'ko',\n    'nl',\n    'pt',\n    'ru',\n    'tr',\n    'uk',\n    'zh',\n  ].contains(locale.languageCode);\n\n  @override\n  bool shouldReload(_AppLocalizationsDelegate old) => false;\n}\n\nAppLocalizations lookupAppLocalizations(Locale locale) {\n  // Lookup logic when language+country codes are specified.\n  switch (locale.languageCode) {\n    case 'zh':\n      {\n        switch (locale.countryCode) {\n          case 'TW':\n            return AppLocalizationsZhTw();\n        }\n        break;\n      }\n  }\n\n  // Lookup logic when only language code is specified.\n  switch (locale.languageCode) {\n    case 'de':\n      return AppLocalizationsDe();\n    case 'en':\n      return AppLocalizationsEn();\n    case 'es':\n      return AppLocalizationsEs();\n    case 'fr':\n      return AppLocalizationsFr();\n    case 'id':\n      return AppLocalizationsId();\n    case 'it':\n      return AppLocalizationsIt();\n    case 'ja':\n      return AppLocalizationsJa();\n    case 'ko':\n      return AppLocalizationsKo();\n    case 'nl':\n      return AppLocalizationsNl();\n    case 'pt':\n      return AppLocalizationsPt();\n    case 'ru':\n      return AppLocalizationsRu();\n    case 'tr':\n      return AppLocalizationsTr();\n    case 'uk':\n      return AppLocalizationsUk();\n    case 'zh':\n      return AppLocalizationsZh();\n  }\n\n  throw FlutterError(\n    '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}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_de.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for German (`de`).\nclass AppLocalizationsDe extends AppLocalizations {\n  AppLocalizationsDe([String locale = 'de']) : super(locale);\n\n  @override\n  String get aboutThanks =>\n      'Vielen Dank an die folgenden Personen, die daran teilgenommen haben.\\n';\n\n  @override\n  String get acceptBeta => 'Akzeptieren Sie Testversion-Updates';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?';\n\n  @override\n  String get added2List => 'Zur Aufgabenliste hinzugefügt';\n\n  @override\n  String get addr => 'Adresse';\n\n  @override\n  String get alreadyLastDir => 'Bereits im letzten Verzeichnis.';\n\n  @override\n  String get askAi => 'KI fragen';\n\n  @override\n  String get askAiApiKey => 'API-Schlüssel';\n\n  @override\n  String get askAiAwaitingResponse => 'Warte auf KI-Antwort...';\n\n  @override\n  String get askAiBaseUrl => 'Basis-URL';\n\n  @override\n  String get askAiCommandInserted => 'Befehl ins Terminal eingefügt';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Bitte konfigurieren Sie $fields in den Einstellungen.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Vor Ausführung bestätigen';\n\n  @override\n  String get askAiConversation => 'KI-Unterhaltung';\n\n  @override\n  String get askAiDisclaimer =>\n      'KI kann Fehler machen. Bitte vorsichtig verwenden.';\n\n  @override\n  String get askAiFollowUpHint => 'Weitere Frage stellen...';\n\n  @override\n  String get askAiInsertTerminal => 'In Terminal einfügen';\n\n  @override\n  String get askAiNoResponse => 'Keine Antwort';\n\n  @override\n  String get askAiRecommendedCommand => 'KI-empfohlener Befehl';\n\n  @override\n  String get askAiSelectedContent => 'Ausgewählter Inhalt';\n\n  @override\n  String get askAiUsageHint => 'Verwendet im SSH-Terminal';\n\n  @override\n  String get atLeastOneTab => 'Mindestens ein Tab muss ausgewählt sein';\n\n  @override\n  String get authFailTip =>\n      'Authentifizierung fehlgeschlagen, bitte überprüfen Sie, ob das Passwort/Schlüssel/Host/Benutzer usw. falsch sind.';\n\n  @override\n  String get autoBackupConflict =>\n      'Es kann nur eine automatische Sicherung gleichzeitig aktiviert werden.';\n\n  @override\n  String get autoConnect => 'Automatisch verbinden';\n\n  @override\n  String get autoRun => 'Automatischer Start';\n\n  @override\n  String get autoUpdateHomeWidget => 'Home-Widget automatisch aktualisieren';\n\n  @override\n  String get availableTabs => 'Verfügbare Tabs';\n\n  @override\n  String get backupEncrypted => 'Backup ist verschlüsselt';\n\n  @override\n  String get backupNotEncrypted => 'Backup ist nicht verschlüsselt';\n\n  @override\n  String get backupPassword => 'Backup-Passwort';\n\n  @override\n  String get backupPasswordRemoved => 'Backup-Passwort entfernt';\n\n  @override\n  String get backupPasswordSet => 'Backup-Passwort gesetzt';\n\n  @override\n  String get backupPasswordTip =>\n      'Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.';\n\n  @override\n  String get backupPasswordWrong => 'Falsches Backup-Passwort';\n\n  @override\n  String get backupTip =>\n      'Die exportierten Daten können mit einem Passwort verschlüsselt werden. \\nBitte sicher aufbewahren.';\n\n  @override\n  String get backupVersionNotMatch =>\n      'Die Backup-Version stimmt nicht überein.';\n\n  @override\n  String get bgRun => 'Hintergrundaktualisierung';\n\n  @override\n  String get bgRunTip =>\n      'Dieser Schalter bedeutet nur, dass die App versuchen wird, im Hintergrund zu laufen. Ob sie im Hintergrund laufen kann, hängt davon ab, ob die Berechtigungen aktiviert sind oder nicht. Bei nativem Android deaktivieren Sie bitte \\\"Batterieoptimierung\\\" in dieser App, und bei miui ändern Sie bitte die Energiesparrichtlinie auf \\\"Unbegrenzt\\\".';\n\n  @override\n  String get clearAllStatsContent =>\n      'Sind Sie sicher, dass Sie alle Server-Verbindungsstatistiken löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.';\n\n  @override\n  String get clearAllStatsTitle => 'Alle Statistiken löschen';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return 'Sind Sie sicher, dass Sie die Verbindungsstatistiken für Server \\\"$serverName\\\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return '$serverName Statistiken löschen';\n  }\n\n  @override\n  String get clearThisServerStats => 'Statistiken dieses Servers löschen';\n\n  @override\n  String get compactDatabase => 'Datenbank komprimieren';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Datenbankgröße: $size\\n\\nDies wird die Datenbank neu organisieren, um die Dateigröße zu reduzieren. Es werden keine Daten gelöscht.';\n  }\n\n  @override\n  String get closeAfterSave => 'Speichern und schließen';\n\n  @override\n  String get collapseUITip =>\n      'Ob lange Listen in der Benutzeroberfläche standardmäßig eingeklappt werden sollen oder nicht';\n\n  @override\n  String get connectionDetails => 'Verbindungsdetails';\n\n  @override\n  String get connectionStats => 'Verbindungsstatistiken';\n\n  @override\n  String get connectionStatsDesc =>\n      'Server-Verbindungserfolgsrate und Verlauf anzeigen';\n\n  @override\n  String get containerTrySudoTip =>\n      'Zum Beispiel: In der App ist der Benutzer auf aaa eingestellt, aber Docker ist unter dem Root-Benutzer installiert. In diesem Fall müssen Sie diese Option aktivieren';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Ein sudo-Passwort ist erforderlich, um auf Docker zuzugreifen. Bitte geben Sie Ihr Passwort ein.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'Das sudo-Passwort ist falsch oder nicht erlaubt. Bitte versuchen Sie es erneut.';\n\n  @override\n  String get convert => 'Konvertieren';\n\n  @override\n  String get copyPath => 'Pfad kopieren';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Zeigen Sie die Auslastung jedes CPUs in einem Fortschrittsbalken-Stil an (alter Stil)';\n\n  @override\n  String get cursorType => 'Cursor-Typ';\n\n  @override\n  String get customCmd => 'Benutzerdefinierte Befehle';\n\n  @override\n  String get customCmdHint => '\\\"Befehlsname\\\": \\\"Befehl\\\"';\n\n  @override\n  String get deleteServers => 'Batch-Löschung von Servern';\n\n  @override\n  String get desktopTerminalTip =>\n      'Befehl zum Öffnen des Terminal-Emulators beim Starten von SSH-Sitzungen.';\n\n  @override\n  String get dirEmpty => 'Stelle sicher, dass der Ordner leer ist.';\n\n  @override\n  String get discoverSshServers => 'SSH-Server entdecken';\n\n  @override\n  String get discoveryFailed => 'Entdeckung fehlgeschlagen';\n\n  @override\n  String get discoverySettings => 'Entdeckungseinstellungen';\n\n  @override\n  String get discoverySummary => 'Entdeckungs-Zusammenfassung';\n\n  @override\n  String get diskHealth => 'Festplattengesundheit';\n\n  @override\n  String get diskIgnorePath => 'Pfad für Datenträger ignorieren';\n\n  @override\n  String get displayCpuIndex => 'Zeigen Sie den CPU-Index an';\n\n  @override\n  String dl2Local(Object fileName) {\n    return 'Datei \\\"$fileName\\\" herunterladen?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'Es gibt keine laufenden Container.\\nDas könnte daran liegen:\\n- Der Docker-Installationsbenutzer ist nicht mit dem in der App konfigurierten Benutzernamen identisch.\\n- Die Umgebungsvariable DOCKER_HOST wurde nicht korrekt gelesen. Sie können sie ermitteln, indem Sie `echo \\$DOCKER_HOST` im Terminal ausführen.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '$count Image(s)';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker ist nicht installiert';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount aktiv, $stoppedCount container gestoppt.';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count Container aktiv';\n  }\n\n  @override\n  String get doubleColumnMode => 'Doppelspaltiger Modus';\n\n  @override\n  String get doubleColumnTip =>\n      'Diese Option aktiviert nur die Funktion, ob sie tatsächlich aktiviert werden kann, hängt auch von der Breite des Geräts ab';\n\n  @override\n  String get editVirtKeys => 'Virtuelle Tasten bearbeiten';\n\n  @override\n  String get editorHighlightTip =>\n      'Die Leistung der aktuellen Codehervorhebung ist schlechter und kann zur Verbesserung optional ausgeschaltet werden.';\n\n  @override\n  String get enableMdns => 'mDNS aktivieren';\n\n  @override\n  String get enableMdnsDesc =>\n      'mDNS/Bonjour verwenden, um SSH-Dienste zu entdecken';\n\n  @override\n  String get envVars => 'Umgebungsvariable';\n\n  @override\n  String get experimentalFeature => 'Experimentelles Feature';\n\n  @override\n  String get extraArgs => 'Extra args';\n\n  @override\n  String get fallbackSshDest => 'SSH-Fallback-Ziel';\n\n  @override\n  String get fdroidReleaseTip =>\n      'Wenn Sie diese App von F-Droid heruntergeladen haben, wird empfohlen, diese Option zu deaktivieren.';\n\n  @override\n  String get fgService => 'Vordergrund-Dienst';\n\n  @override\n  String get fgServiceTip =>\n      'Nach dem Einschalten kann es bei einigen Gerätemodellen zu Abstürzen kommen. Das Ausschalten kann bei einigen Modellen dazu führen, dass SSH-Verbindungen im Hintergrund nicht aufrechterhalten werden können. Bitte erlauben Sie ServerBox in den Systemeinstellungen Benachrichtigungsrechte, Hintergrundausführung und Selbstaktivierung.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'Datei \\'$file\\' ist zu groß $size, max $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Beendet um';\n\n  @override\n  String get followSystem => 'System verfolgen';\n\n  @override\n  String get fontSize => 'Schriftgröße';\n\n  @override\n  String get fullScreen => 'Vollbildmodus';\n\n  @override\n  String get fullScreenJitter => 'Jitter im Vollbildmodus';\n\n  @override\n  String get fullScreenJitterHelp => 'Einbrennen des Bildschirms verhindern';\n\n  @override\n  String get fullScreenTip =>\n      'Soll der Vollbildmodus aktiviert werden, wenn das Gerät in den Quermodus gedreht wird? Diese Option gilt nur für die Server-Registerkarte.';\n\n  @override\n  String get goBackQ => 'Zurückkommen?';\n\n  @override\n  String get goto => 'Pfad öffnen';\n\n  @override\n  String get hideTitleBar => 'Titelleiste ausblenden';\n\n  @override\n  String get highlight => 'Code highlight';\n\n  @override\n  String get homeTabs => 'Home-Tabs';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Passen Sie an, welche Tabs auf der Startseite angezeigt werden und ihre Reihenfolge';\n\n  @override\n  String get homeWidgetUrlConfig => 'Home-Widget-Link konfigurieren';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'Anfrage fehlgeschlagen, Statuscode: $code';\n  }\n\n  @override\n  String get ignoreCert => 'Zertifikat ignorieren';\n\n  @override\n  String get image => 'Image';\n\n  @override\n  String get imagesList => 'Images';\n\n  @override\n  String get installDockerWithUrl =>\n      'Bitte installiere docker zuerst. https://docs.docker.com/engine/install';\n\n  @override\n  String get invalid => 'Ungültig';\n\n  @override\n  String get invalidHostFormat =>\n      'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.';\n\n  @override\n  String get jumpServer => 'Server springen';\n\n  @override\n  String get keepForeground => 'Stelle sicher, dass die App geöffnet bleibt.';\n\n  @override\n  String get keepStatusWhenErr => 'Den letzten Serverstatus beibehalten';\n\n  @override\n  String get keepStatusWhenErrTip =>\n      'Nur im Fehlerfall während der Ausführung des Skripts';\n\n  @override\n  String get keyAuth => 'Schlüsselauthentifzierung';\n\n  @override\n  String get lastFailure => 'Letzter Fehler';\n\n  @override\n  String get lastSuccess => 'Letzter Erfolg';\n\n  @override\n  String get letterCache => 'Buchstaben-Caching';\n\n  @override\n  String get letterCacheTip =>\n      'Empfohlen, zu deaktivieren, aber nach dem Deaktivieren können keine CJK-Zeichen eingegeben werden.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return 'Erstellt mit ❤️ von $myGithub';\n  }\n\n  @override\n  String get max => 'max';\n\n  @override\n  String get maxConcurrency => 'Maximale Gleichzeitigkeit';\n\n  @override\n  String get maxRetryCount => 'Anzahl an Verbindungsversuchen';\n\n  @override\n  String get maxRetryCountEqual0 =>\n      'Unbegrenzte Verbindungsversuche zum Server';\n\n  @override\n  String get min => 'min';\n\n  @override\n  String get more => 'Mehr';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'Ein: kann unter jeder Karte auf der Registerkarte \\\"Server\\\" angezeigt werden. Aus: kann oben auf der Seite \\\"Serverdetails\\\" angezeigt werden.';\n\n  @override\n  String get needHomeDir =>\n      'Wenn Sie ein Synology-Benutzer sind, [sehen Sie hier](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Benutzer anderer Systeme müssen suchen, wie man ein Home-Verzeichnis erstellt.';\n\n  @override\n  String get needRestart => 'App muss neugestartet werden';\n\n  @override\n  String get netViewType => 'Netzwerkansicht Typ';\n\n  @override\n  String get newContainer => 'Neuer Container';\n\n  @override\n  String get noConnectionStatsData => 'Keine Verbindungsstatistikdaten';\n\n  @override\n  String get noLineChart => 'Verwenden Sie keine Liniendiagramme';\n\n  @override\n  String get noLineChartForCpu => 'Verwenden Sie keine Liniendiagramme für CPU';\n\n  @override\n  String get noPrivateKeyTip =>\n      'Der private Schlüssel existiert nicht, möglicherweise wurde er gelöscht oder es liegt ein Konfigurationsfehler vor.';\n\n  @override\n  String get noPromptAgain => 'Nicht mehr nachfragen';\n\n  @override\n  String get onServerDetailPage => 'in Detailansicht des Servers';\n\n  @override\n  String get onlyOneLine => 'Nur als eine Zeile anzeigen (scrollbar)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 =>\n      'Wirksam nur, wenn die Anzahl der Kerne > 8 ist.';\n\n  @override\n  String get openLastPath => 'Öffnen Sie den letzten Pfad';\n\n  @override\n  String get openLastPathTip =>\n      'Verschiedene Server haben unterschiedliche Einträge, und der Eintrag ist der Pfad zum Ausgang';\n\n  @override\n  String get parseContainerStatsTip =>\n      'Das Analysieren des Belegungsstatus durch Docker ist relativ langsam';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$percent% von $size';\n  }\n\n  @override\n  String get permission => 'Berechtigungen';\n\n  @override\n  String get pingInputIP => 'Bitte gib eine Ziel-IP/Domain ein.';\n\n  @override\n  String get pingNoServer =>\n      'Kein Server zum Anpingen.\\nBitte füge einen Server hinzu.';\n\n  @override\n  String get plugInType => 'Einfügetyp';\n\n  @override\n  String get preferDiskAmount => 'Festplattenkapazität vorrangig anzeigen';\n\n  @override\n  String get privateKey => 'Private Key';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'Privater Schlüssel [$keyId] wurde nicht gefunden.';\n  }\n\n  @override\n  String get pushToken => 'Push Token';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'Nicht empfohlen, Achten Sie auf Sicherheitsrisiken! Wenn Sie das Standardzertifikat von PVE verwenden, müssen Sie diese Option aktivieren.';\n\n  @override\n  String get pveLoginFailed =>\n      'Anmeldung fehlgeschlagen. Kann nicht mit Benutzername/Passwort aus der Serverkonfiguration angemeldet werden, um sich über Linux PAM anzumelden.';\n\n  @override\n  String get pveVersionLow =>\n      'Diese Funktion befindet sich derzeit in der Testphase und wurde nur auf PVE 8+ getestet. Bitte verwenden Sie sie mit Vorsicht.';\n\n  @override\n  String get read => 'Lesen';\n\n  @override\n  String get recentConnections => 'Kürzliche Verbindungen';\n\n  @override\n  String get rememberPwdInMem => 'Passwort im Speicher behalten';\n\n  @override\n  String get rememberPwdInMemTip => 'Für Container, Aufhängen usw.';\n\n  @override\n  String get rememberWindowSize => 'Fenstergröße merken';\n\n  @override\n  String get remotePath => 'Entfernte Pfade';\n\n  @override\n  String get result => 'Result';\n\n  @override\n  String get rotateAngel => 'Rotationswinkel';\n\n  @override\n  String get sameIdServerExist =>\n      'Ein Server mit derselben ID existiert bereits';\n\n  @override\n  String get save => 'Speichern';\n\n  @override\n  String get second => 's';\n\n  @override\n  String get serverDetailOrder => 'Reihenfolge der Widgets auf der Detailseite';\n\n  @override\n  String get serverFuncBtns => 'Server-Funktionsschaltflächen';\n\n  @override\n  String get serverOrder => 'Server-Bestellung';\n\n  @override\n  String get serverTabRequired => 'Server-Tab kann nicht entfernt werden';\n\n  @override\n  String get sftpDlPrepare => 'Verbindung vorbereiten...';\n\n  @override\n  String get sftpEditorTip =>\n      'Wenn leer, verwenden Sie den im App integrierten Dateieditor. Wenn ein Wert vorhanden ist, wird der Editor des Remote-Servers verwendet, z.B. `vim` (es wird empfohlen, automatisch gemäß `EDITOR` zu ermitteln).';\n\n  @override\n  String get sftpRmrDirSummary =>\n      'Verwenden Sie \\\"rm -r\\\", um das Verzeichnis in SFTP zu löschen.';\n\n  @override\n  String get sftpSSHConnected => 'SFTP Verbunden';\n\n  @override\n  String get sftpShowFoldersFirst => 'Ordner zuerst anzeigen';\n\n  @override\n  String get showDistLogo => 'Distributionslogo anzeigen';\n\n  @override\n  String get size => 'Größe';\n\n  @override\n  String get softWrap => 'Weicher Umbruch';\n\n  @override\n  String get specifyDev => 'Gerät angeben';\n\n  @override\n  String get specifyDevTip =>\n      'Zum Beispiel bezieht sich die Standard-Netzwerkverkehrsstatistik auf alle Geräte. Hier können Sie ein bestimmtes Gerät angeben.';\n\n  @override\n  String get speed => 'Tempo';\n\n  @override\n  String spentTime(Object time) {\n    return 'Benötigte Zeit: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'Alle Server existieren bereits ($duplicateCount Duplikate gefunden)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount Duplikate werden übersprungen';\n  }\n\n  @override\n  String get sshConfigFound =>\n      'Wir haben SSH-Konfiguration auf Ihrem System gefunden.';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return '$totalCount Server gefunden';\n  }\n\n  @override\n  String get sshConfigImport => 'SSH-Konfiguration importieren';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Es können nur Basisinformationen importiert werden, zum Beispiel: IP/Port.';\n\n  @override\n  String get sshConfigImportPermission =>\n      'Möchten Sie die Berechtigung erteilen, ~/.ssh/config zu lesen und Server-Einstellungen automatisch zu importieren?';\n\n  @override\n  String get sshConfigImportTip =>\n      'Bei der ersten Server-Erstellung zum Lesen von ~/.ssh/config auffordern';\n\n  @override\n  String sshConfigImported(Object count) {\n    return '$count Server aus SSH-Konfiguration importiert';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return 'Der SSH-Hostschlüssel für $serverName hat sich geändert. Fahren Sie nur fort, wenn Sie diesem Server vertrauen.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Fingerabdruck (MD5 Base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Fingerabdruck (MD5 Hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'SSH-Hostschlüsseltyp';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return 'Ein neuer SSH-Hostschlüssel wurde von $serverName empfangen. Prüfen Sie den Fingerabdruck, bevor Sie vertrauen.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Gespeicherter Fingerabdruck: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      'Möchten Sie die SSH-Konfigurationsdatei manuell auswählen?';\n\n  @override\n  String get sshConfigNoServers =>\n      'Keine Server in der SSH-Konfiguration gefunden';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'Aufgrund der macOS-Berechtigungen kann nicht auf die SSH-Konfigurationsdatei zugegriffen werden.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount Server werden importiert';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'Wenn das Terminal scrollbar ist, kann durch horizontales Ziehen Text ausgewählt werden. Durch Klicken auf die Tastentaste wird die Tastatur ein- oder ausgeschaltet. Das Dateisymbol öffnet den aktuellen Pfad SFTP. Die Zwischenablage-Schaltfläche kopiert den Inhalt, wenn Text ausgewählt ist, und fügt Inhalte aus der Zwischenablage in das Terminal ein, wenn kein Text ausgewählt ist und Inhalte in der Zwischenablage vorhanden sind. Das Codesymbol fügt Code-Schnipsel ins Terminal ein und führt sie aus.';\n\n  @override\n  String sshTip(Object url) {\n    return 'Diese Funktion befindet sich jetzt in der Experimentierphase.\\n\\nBitte melde Bugs auf $url oder mach mit bei der Entwicklung.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff =>\n      'Automatische Umschaltung der virtuellen Tasten';\n\n  @override\n  String get stat => 'Statistik';\n\n  @override\n  String get supportFmtArgs =>\n      'Die folgenden Formatierungsparameter werden unterstützt:';\n\n  @override\n  String get suspendTip =>\n      'Die Suspend-Funktion erfordert Root-Rechte und systemd-Unterstützung.';\n\n  @override\n  String switchTo(Object val) {\n    return 'Wechseln zu $val';\n  }\n\n  @override\n  String get syncTip =>\n      'Damit einige Änderungen wirksam werden, kann ein Neustart erforderlich sein.';\n\n  @override\n  String get system => 'Systeme';\n\n  @override\n  String get tag => 'Tags';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Tippen Sie auf die Suche-Schaltfläche, um SSH-Server in Ihrem Netzwerk zu entdecken';\n\n  @override\n  String get termFontSizeTip =>\n      'Diese Einstellung beeinflusst die Größe des Terminals (Breite und Höhe). Sie können die Terminalseite zoomen, um die Schriftgröße der aktuellen Sitzung anzupassen.';\n\n  @override\n  String get textScaler => 'Skalierung der Schriftart';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (Originalgröße), funktioniert nur auf der Serverseite Teil der Schrift, nicht empfohlen zu ändern.';\n\n  @override\n  String get time => 'Zeit';\n\n  @override\n  String get times => 'x';\n\n  @override\n  String get trySudo => 'Versuche es mit sudo';\n\n  @override\n  String get unknown => 'Unbekannt';\n\n  @override\n  String get unkownConvertMode => 'Unbekannter Konvertierungsmodus';\n\n  @override\n  String get update => 'Update';\n\n  @override\n  String get updateIntervalEqual0 =>\n      'Wenn du den Wert 0 einstellst, wird nicht automatisch aktualisiert.\\nDer CPU-Status kann nicht berechnet werden.';\n\n  @override\n  String get updateServerStatusInterval =>\n      'Aktualisierungsintervall des Serverstatus';\n\n  @override\n  String get upsideDown => 'Upside Down';\n\n  @override\n  String get useCdn => 'Verwenden von CDN';\n\n  @override\n  String get useCdnTip =>\n      'Nicht-chinesischen Benutzern wird die Verwendung eines CDN empfohlen. Möchten Sie es verwenden?';\n\n  @override\n  String get useNoPwd => 'Es wird kein Passwort verwendet';\n\n  @override\n  String get usePodmanByDefault => 'Standardmäßige Verwendung von Podman';\n\n  @override\n  String get used => 'Gebraucht';\n\n  @override\n  String get view => 'Ansicht';\n\n  @override\n  String get viewDetails => 'Details anzeigen';\n\n  @override\n  String get viewErr => 'Fehler anzeigen';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'In die Zwischenablage kopieren, wenn das ausgewählte Terminal nicht leer ist, andernfalls den Inhalt der Zwischenablage in das Terminal einfügen.';\n\n  @override\n  String get virtKeyHelpIME => 'Tastatur ein-/ausschalten';\n\n  @override\n  String get virtKeyHelpSFTP => 'Aktuelles Verzeichnis in SFTP öffnen.';\n\n  @override\n  String get waitConnection =>\n      'Bitte warte, bis die Verbindung hergestellt wurde.';\n\n  @override\n  String get wakeLock => 'Wach halten';\n\n  @override\n  String get watchNotPaired => 'Keine gekoppelte Apple Watch';\n\n  @override\n  String get webdavSettingEmpty => 'Webdav-Einstellungen sind leer';\n\n  @override\n  String get whenOpenApp => 'Beim Öffnen der App';\n\n  @override\n  String get wolTip =>\n      'Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.';\n\n  @override\n  String get write => 'Schreiben';\n\n  @override\n  String get writeScriptFailTip =>\n      'Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.';\n\n  @override\n  String get writeScriptTip =>\n      'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \\n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Podman Docker-Emulation erkannt. Bitte wechseln Sie in den Einstellungen zu Podman.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_en.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.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 aboutThanks =>\n      'Thanks to the following people who participated in.';\n\n  @override\n  String get acceptBeta => 'Accept beta version updates';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'Currently private keys don\\'t exist, do you want to add the one that comes with the system (~/.ssh/id_rsa)?';\n\n  @override\n  String get added2List => 'Added to task list';\n\n  @override\n  String get addr => 'Address';\n\n  @override\n  String get alreadyLastDir => 'Already in last directory.';\n\n  @override\n  String get askAi => 'Ask AI';\n\n  @override\n  String get askAiApiKey => 'API Key';\n\n  @override\n  String get askAiAwaitingResponse => 'Waiting for AI response...';\n\n  @override\n  String get askAiBaseUrl => 'Base URL';\n\n  @override\n  String get askAiCommandInserted => 'Command inserted into terminal';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Please configure $fields in Settings.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Confirm before executing';\n\n  @override\n  String get askAiConversation => 'AI conversation';\n\n  @override\n  String get askAiDisclaimer =>\n      'AI may be incorrect. Review carefully before applying.';\n\n  @override\n  String get askAiFollowUpHint => 'Ask a follow-up...';\n\n  @override\n  String get askAiInsertTerminal => 'Insert into terminal';\n\n  @override\n  String get askAiNoResponse => 'No response';\n\n  @override\n  String get askAiRecommendedCommand => 'AI suggested command';\n\n  @override\n  String get askAiSelectedContent => 'Selected content';\n\n  @override\n  String get askAiUsageHint => 'Used in SSH Terminal';\n\n  @override\n  String get atLeastOneTab => 'At least one tab must be selected';\n\n  @override\n  String get authFailTip =>\n      'Authentication failed, please check whether credentials are correct';\n\n  @override\n  String get autoBackupConflict =>\n      'Only one automatic backup can be turned on at the same time.';\n\n  @override\n  String get autoConnect => 'Auto connect';\n\n  @override\n  String get autoRun => 'Auto run';\n\n  @override\n  String get autoUpdateHomeWidget => 'Automatic home widget update';\n\n  @override\n  String get availableTabs => 'Available Tabs';\n\n  @override\n  String get backupEncrypted => 'Backup is encrypted';\n\n  @override\n  String get backupNotEncrypted => 'Backup is not encrypted';\n\n  @override\n  String get backupPassword => 'Backup password';\n\n  @override\n  String get backupPasswordRemoved => 'Backup password removed';\n\n  @override\n  String get backupPasswordSet => 'Backup password set';\n\n  @override\n  String get backupPasswordTip =>\n      'Set a password to encrypt backup files. Leave empty to disable encryption.';\n\n  @override\n  String get backupPasswordWrong => 'Incorrect backup password';\n\n  @override\n  String get backupTip =>\n      'The exported data can be encrypted with password. \\nPlease keep it safe.';\n\n  @override\n  String get backupVersionNotMatch => 'Backup version is not match.';\n\n  @override\n  String get bgRun => 'Run in background';\n\n  @override\n  String get bgRunTip =>\n      'This switch only means the program will try to run in the background. Whether it can run in the background depends on whether the permission is enabled or not. For AOSP-based Android ROMs, please disable \\\"Battery Optimization\\\" in this app. For MIUI / HyperOS, please change the power saving policy to \\\"Unlimited\\\".';\n\n  @override\n  String get clearAllStatsContent =>\n      'Are you sure you want to clear all server connection statistics? This action cannot be undone.';\n\n  @override\n  String get clearAllStatsTitle => 'Clear All Statistics';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return 'Are you sure you want to clear connection statistics for server \\\"$serverName\\\"? This action cannot be undone.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return 'Clear $serverName Statistics';\n  }\n\n  @override\n  String get clearThisServerStats => 'Clear This Server Statistics';\n\n  @override\n  String get compactDatabase => 'Compact Database';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Database size: $size\\n\\nThis will reorganize the database to reduce file size. No data will be deleted.';\n  }\n\n  @override\n  String get closeAfterSave => 'Save and close';\n\n  @override\n  String get collapseUITip =>\n      'Whether to collapse long lists present in the UI by default';\n\n  @override\n  String get connectionDetails => 'Connection Details';\n\n  @override\n  String get connectionStats => 'Connection Statistics';\n\n  @override\n  String get connectionStatsDesc =>\n      'View server connection success rate and history';\n\n  @override\n  String get containerTrySudoTip =>\n      'For example: In the app, the user is set to aaa, but Docker is installed under the root user. In this case, you need to enable this option.';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Sudo password is required to access Docker. Please enter your password.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'Sudo password is incorrect or not allowed. Please try again.';\n\n  @override\n  String get convert => 'Convert';\n\n  @override\n  String get copyPath => 'Copy path';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Display the usage of each CPU in a progress bar style (old style)';\n\n  @override\n  String get cursorType => 'Cursor type';\n\n  @override\n  String get customCmd => 'Custom commands';\n\n  @override\n  String get customCmdHint => '\\\"Command Name\\\": \\\"Command\\\"';\n\n  @override\n  String get deleteServers => 'Batch delete servers';\n\n  @override\n  String get desktopTerminalTip =>\n      'Command used to open the terminal emulator when launching SSH sessions.';\n\n  @override\n  String get dirEmpty => 'Make sure the folder is empty.';\n\n  @override\n  String get discoverSshServers => 'Discover SSH Servers';\n\n  @override\n  String get discoveryFailed => 'Discovery failed';\n\n  @override\n  String get discoverySettings => 'Discovery Settings';\n\n  @override\n  String get discoverySummary => 'Discovery Summary';\n\n  @override\n  String get diskHealth => 'Disk Health';\n\n  @override\n  String get diskIgnorePath => 'Ignore path for disk';\n\n  @override\n  String get displayCpuIndex => 'Display CPU index';\n\n  @override\n  String dl2Local(Object fileName) {\n    return 'Download $fileName to local?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'There are no running containers.\\nThis could be because:\\n- The Docker installation user is not the same as the username configured within the App.\\n- The environment variable DOCKER_HOST was not read correctly. You can get it by running `echo \\$DOCKER_HOST` in the terminal.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '$count images';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker not installed';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount running, $stoppedCount container stopped.';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count container running.';\n  }\n\n  @override\n  String get doubleColumnMode => 'Double column mode';\n\n  @override\n  String get doubleColumnTip =>\n      'This option only enables the feature, whether it can actually be enabled depends on the width of the device';\n\n  @override\n  String get editVirtKeys => 'Edit virtual keys';\n\n  @override\n  String get editorHighlightTip =>\n      'The current code highlighting performance is not ideal and can be optionally turned off to improve.';\n\n  @override\n  String get enableMdns => 'Enable mDNS';\n\n  @override\n  String get enableMdnsDesc => 'Use mDNS/Bonjour to discover SSH services';\n\n  @override\n  String get envVars => 'Environment variable';\n\n  @override\n  String get experimentalFeature => 'Experimental feature';\n\n  @override\n  String get extraArgs => 'Extra arguments';\n\n  @override\n  String get fallbackSshDest => 'Fallback SSH destination';\n\n  @override\n  String get fdroidReleaseTip =>\n      'If you downloaded this app from F-Droid, it is recommended to turn off this option.';\n\n  @override\n  String get fgService => 'Foreground Service';\n\n  @override\n  String get fgServiceTip =>\n      'After enabling, some device models may crash. Disabling it may cause some models to be unable to maintain SSH connections in the background. Please allow ServerBox notification permissions, background running, and self-wake-up in system settings.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'File \\'$file\\' too large $size, max $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Finished at';\n\n  @override\n  String get followSystem => 'Follow system';\n\n  @override\n  String get fontSize => 'Font size';\n\n  @override\n  String get fullScreen => 'Full screen mode';\n\n  @override\n  String get fullScreenJitter => 'Full screen jitter';\n\n  @override\n  String get fullScreenJitterHelp => 'To avoid screen burn-in';\n\n  @override\n  String get fullScreenTip =>\n      'Should full-screen mode be enabled when the device is rotated to landscape mode? This option only applies to the server tab.';\n\n  @override\n  String get goBackQ => 'Go back?';\n\n  @override\n  String get goto => 'Go to';\n\n  @override\n  String get hideTitleBar => 'Hide title bar';\n\n  @override\n  String get highlight => 'Code highlighting';\n\n  @override\n  String get homeTabs => 'Home Tabs';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Customize which tabs appear on the home page and their order';\n\n  @override\n  String get homeWidgetUrlConfig => 'Config home widget url';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'request failed, status code: $code';\n  }\n\n  @override\n  String get ignoreCert => 'Ignore certificate';\n\n  @override\n  String get image => 'Image';\n\n  @override\n  String get imagesList => 'Images list';\n\n  @override\n  String get installDockerWithUrl =>\n      'Please https://docs.docker.com/engine/install docker first.';\n\n  @override\n  String get invalid => 'Invalid';\n\n  @override\n  String get invalidHostFormat =>\n      'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.';\n\n  @override\n  String get jumpServer => 'Jump server';\n\n  @override\n  String get keepForeground => 'Keep app foreground!';\n\n  @override\n  String get keepStatusWhenErr => 'Preserve the last server state';\n\n  @override\n  String get keepStatusWhenErrTip =>\n      'Only in the event of an error during script execution';\n\n  @override\n  String get keyAuth => 'Key Auth';\n\n  @override\n  String get lastFailure => 'Last Failure';\n\n  @override\n  String get lastSuccess => 'Last Success';\n\n  @override\n  String get letterCache => 'Letter caching';\n\n  @override\n  String get letterCacheTip =>\n      'Recommended to disable, but after disabling, it will be impossible to input CJK characters.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return 'Made with ❤️ by $myGithub';\n  }\n\n  @override\n  String get max => 'max';\n\n  @override\n  String get maxConcurrency => 'Max Concurrency';\n\n  @override\n  String get maxRetryCount => 'Number of server reconnections';\n\n  @override\n  String get maxRetryCountEqual0 => 'Will retry again and again.';\n\n  @override\n  String get min => 'min';\n\n  @override\n  String get more => 'More';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.';\n\n  @override\n  String get needHomeDir =>\n      'If you are a Synology user, [see here](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Users of other systems need to search for how to create a home directory.';\n\n  @override\n  String get needRestart => 'App needs to be restarted';\n\n  @override\n  String get netViewType => 'Network view type';\n\n  @override\n  String get newContainer => 'New container';\n\n  @override\n  String get noConnectionStatsData => 'No connection statistics data';\n\n  @override\n  String get noLineChart => 'Do not use line charts';\n\n  @override\n  String get noLineChartForCpu => 'Do not use line charts for CPU';\n\n  @override\n  String get noPrivateKeyTip =>\n      'The private key does not exist, it may have been deleted or there is a configuration error.';\n\n  @override\n  String get noPromptAgain => 'Do not prompt again';\n\n  @override\n  String get onServerDetailPage => 'On server detail page';\n\n  @override\n  String get onlyOneLine => 'Only display as one line (scrollable)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 =>\n      'Works only when the number of cores is greater than 8';\n\n  @override\n  String get openLastPath => 'Open the last path';\n\n  @override\n  String get openLastPathTip =>\n      'Different servers will have different logs, and the log is the path to the exit';\n\n  @override\n  String get parseContainerStatsTip =>\n      'Parsing the occupancy status of Docker is relatively slow.';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$percent% of $size';\n  }\n\n  @override\n  String get permission => 'Permissions';\n\n  @override\n  String get pingInputIP => 'Please input a target IP / domain.';\n\n  @override\n  String get pingNoServer =>\n      'No server to ping.\\nPlease add a server in server tab.';\n\n  @override\n  String get plugInType => 'Insertion Type';\n\n  @override\n  String get preferDiskAmount => 'Prioritize displaying disk capacity';\n\n  @override\n  String get privateKey => 'Private Key';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'Private key [$keyId] not found.';\n  }\n\n  @override\n  String get pushToken => 'Push token';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'Not recommended to enable, beware of security risks! If you are using the default certificate from PVE, you need to enable this option.';\n\n  @override\n  String get pveLoginFailed =>\n      'Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.';\n\n  @override\n  String get pveVersionLow =>\n      'This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.';\n\n  @override\n  String get read => 'Read';\n\n  @override\n  String get recentConnections => 'Recent Connections';\n\n  @override\n  String get rememberPwdInMem => 'Remember password in memory';\n\n  @override\n  String get rememberPwdInMemTip => 'Used for containers, suspending, etc.';\n\n  @override\n  String get rememberWindowSize => 'Remember window size';\n\n  @override\n  String get remotePath => 'Remote path';\n\n  @override\n  String get result => 'Result';\n\n  @override\n  String get rotateAngel => 'Rotation angle';\n\n  @override\n  String get sameIdServerExist => 'A server with the same ID already exists';\n\n  @override\n  String get save => 'Save';\n\n  @override\n  String get second => 's';\n\n  @override\n  String get serverDetailOrder => 'Detail page widget order';\n\n  @override\n  String get serverFuncBtns => 'Server function buttons';\n\n  @override\n  String get serverOrder => 'Server order';\n\n  @override\n  String get serverTabRequired => 'Server tab cannot be removed';\n\n  @override\n  String get sftpDlPrepare => 'Preparing to connect...';\n\n  @override\n  String get sftpEditorTip =>\n      'If empty, use the built-in file editor of the app. If a value is present, use the remote server’s editor, e.g., `vim` (recommended to automatically detect according to `EDITOR`).';\n\n  @override\n  String get sftpRmrDirSummary => 'Use `rm -r` to delete a folder in SFTP.';\n\n  @override\n  String get sftpSSHConnected => 'SFTP Connected';\n\n  @override\n  String get sftpShowFoldersFirst => 'Display folders first';\n\n  @override\n  String get showDistLogo => 'Show distribution logo';\n\n  @override\n  String get size => 'Size';\n\n  @override\n  String get softWrap => 'Soft wrap';\n\n  @override\n  String get specifyDev => 'Specify device';\n\n  @override\n  String get specifyDevTip =>\n      'For example, network traffic statistics are by default for all devices. You can specify a particular device here.';\n\n  @override\n  String get speed => 'Speed';\n\n  @override\n  String spentTime(Object time) {\n    return 'Spent time: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'All servers already exist ($duplicateCount duplicates found)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount duplicates will be skipped';\n  }\n\n  @override\n  String get sshConfigFound => 'We found SSH configuration on your system.';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return 'Found $totalCount servers';\n  }\n\n  @override\n  String get sshConfigImport => 'SSH Config Import';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Only basic information can be imported, for example: IP/Port.';\n\n  @override\n  String get sshConfigImportPermission =>\n      'Would you like to give permission to read ~/.ssh/config and automatically import server settings?';\n\n  @override\n  String get sshConfigImportTip =>\n      'Prompt to read ~/.ssh/config on first server creation';\n\n  @override\n  String sshConfigImported(Object count) {\n    return 'Imported $count servers from SSH config';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return 'The SSH host key changed for $serverName. Only continue if you trust this server.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Fingerprint (MD5 base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Fingerprint (MD5 hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'SSH host key type';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return 'A new SSH host key was received from $serverName. Review the fingerprint before trusting.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Stored fingerprint: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      'Would you like to select the SSH config file manually?';\n\n  @override\n  String get sshConfigNoServers => 'No servers found in SSH config';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'Cannot access SSH config file due to macOS permissions.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount servers will be imported';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'When the terminal is scrollable, dragging horizontally can select text. Clicking the keyboard button turns the keyboard on/off. The file icon opens the current path SFTP. The clipboard button copies the content when text is selected, and pastes content from the clipboard into the terminal when no text is selected and there is content on the clipboard. The code icon pastes code snippets into the terminal and executes them.';\n\n  @override\n  String sshTip(Object url) {\n    return 'This function is now in the experimental stage.\\n\\nPlease report bugs on $url or join our development.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff => 'Auto switching of virtual keys';\n\n  @override\n  String get stat => 'Statistics';\n\n  @override\n  String get supportFmtArgs =>\n      'The following formatting parameters are supported:';\n\n  @override\n  String get suspendTip =>\n      'The suspend function requires root permission and systemd support.';\n\n  @override\n  String switchTo(Object val) {\n    return 'Switch to $val';\n  }\n\n  @override\n  String get syncTip =>\n      'A restart may be required for some changes to take effect.';\n\n  @override\n  String get system => 'System';\n\n  @override\n  String get tag => 'Tags';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Tap the search button to discover SSH servers on your network';\n\n  @override\n  String get termFontSizeTip =>\n      'This setting will affect the terminal size (width and height). You can zoom in on the terminal page to adjust the font size of the current session.';\n\n  @override\n  String get textScaler => 'Text scaler';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (original size), only works on server page part of the font, not recommended to change.';\n\n  @override\n  String get time => 'Time';\n\n  @override\n  String get times => 'Times';\n\n  @override\n  String get trySudo => 'Try using sudo';\n\n  @override\n  String get unknown => 'Unknown';\n\n  @override\n  String get unkownConvertMode => 'Unknown conversion mode';\n\n  @override\n  String get update => 'Update';\n\n  @override\n  String get updateIntervalEqual0 =>\n      'You set to 0, will not update automatically.\\nCan\\'t calculate CPU status.';\n\n  @override\n  String get updateServerStatusInterval => 'Server status update interval';\n\n  @override\n  String get upsideDown => 'Upside Down';\n\n  @override\n  String get useCdn => 'Using CDN';\n\n  @override\n  String get useCdnTip =>\n      'Non-Chinese users are recommended to use CDN. Would you like to use it?';\n\n  @override\n  String get useNoPwd => 'No password will be used';\n\n  @override\n  String get usePodmanByDefault => 'Use Podman by default';\n\n  @override\n  String get used => 'Used';\n\n  @override\n  String get view => 'View';\n\n  @override\n  String get viewDetails => 'View Details';\n\n  @override\n  String get viewErr => 'See error';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'Copy to the clipboard if the selected terminal is not empty, otherwise paste the content of the clipboard to the terminal.';\n\n  @override\n  String get virtKeyHelpIME => 'Turn on/off the keyboard';\n\n  @override\n  String get virtKeyHelpSFTP => 'Open current directory in SFTP.';\n\n  @override\n  String get waitConnection =>\n      'Please wait for the connection to be established.';\n\n  @override\n  String get wakeLock => 'Keep awake';\n\n  @override\n  String get watchNotPaired => 'No paired Apple Watch';\n\n  @override\n  String get webdavSettingEmpty => 'WebDav setting is empty';\n\n  @override\n  String get whenOpenApp => 'When opening the app';\n\n  @override\n  String get wolTip =>\n      'After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.';\n\n  @override\n  String get write => 'Write';\n\n  @override\n  String get writeScriptFailTip =>\n      'Writing to the script failed, possibly due to lack of permissions or the directory does not exist.';\n\n  @override\n  String get writeScriptTip =>\n      'After connecting to the server, a script will be written to `~/.config/server_box` \\n | `/tmp/server_box` to monitor the system status. You can review the script content.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Podman Docker emulation detected. Please switch to Podman in settings.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_es.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Spanish Castilian (`es`).\nclass AppLocalizationsEs extends AppLocalizations {\n  AppLocalizationsEs([String locale = 'es']) : super(locale);\n\n  @override\n  String get aboutThanks => 'Gracias a los siguientes participantes.';\n\n  @override\n  String get acceptBeta => 'Aceptar actualizaciones de la versión de prueba';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'Actualmente no hay ninguna llave privada, ¿quieres agregar la que viene por defecto en el sistema (~/.ssh/id_rsa)?';\n\n  @override\n  String get added2List => 'Añadido a la lista de tareas';\n\n  @override\n  String get addr => 'Dirección';\n\n  @override\n  String get alreadyLastDir => 'Ya estás en el directorio superior';\n\n  @override\n  String get askAi => 'Preguntar a la IA';\n\n  @override\n  String get askAiApiKey => 'Clave API';\n\n  @override\n  String get askAiAwaitingResponse => 'Esperando la respuesta de la IA...';\n\n  @override\n  String get askAiBaseUrl => 'URL base';\n\n  @override\n  String get askAiCommandInserted => 'Comando insertado en el terminal';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Configura $fields en Ajustes.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Confirmar antes de ejecutar';\n\n  @override\n  String get askAiConversation => 'Conversación con la IA';\n\n  @override\n  String get askAiDisclaimer =>\n      'La IA puede equivocarse. Úsala con precaución.';\n\n  @override\n  String get askAiFollowUpHint => 'Haz una pregunta adicional...';\n\n  @override\n  String get askAiInsertTerminal => 'Insertar en el terminal';\n\n  @override\n  String get askAiNoResponse => 'Sin respuesta';\n\n  @override\n  String get askAiRecommendedCommand => 'Comando sugerido por la IA';\n\n  @override\n  String get askAiSelectedContent => 'Contenido seleccionado';\n\n  @override\n  String get askAiUsageHint => 'Usado en el terminal SSH';\n\n  @override\n  String get atLeastOneTab => 'Al menos una pestaña debe estar seleccionada';\n\n  @override\n  String get authFailTip =>\n      'La autenticación ha fallado, por favor verifica si la contraseña/llave/host/usuario, etc., son incorrectos.';\n\n  @override\n  String get autoBackupConflict =>\n      'Solo se puede activar una copia de seguridad automática a la vez';\n\n  @override\n  String get autoConnect => 'Conexión automática';\n\n  @override\n  String get autoRun => 'Ejecución automática';\n\n  @override\n  String get autoUpdateHomeWidget =>\n      'Actualizar automáticamente el widget del escritorio';\n\n  @override\n  String get availableTabs => 'Pestañas disponibles';\n\n  @override\n  String get backupEncrypted => 'El respaldo está encriptado';\n\n  @override\n  String get backupNotEncrypted => 'El respaldo no está encriptado';\n\n  @override\n  String get backupPassword => 'Contraseña de respaldo';\n\n  @override\n  String get backupPasswordRemoved => 'Contraseña de respaldo eliminada';\n\n  @override\n  String get backupPasswordSet => 'Contraseña de respaldo establecida';\n\n  @override\n  String get backupPasswordTip =>\n      'Establece una contraseña para encriptar archivos de respaldo. Déjalo vacío para desactivar la encriptación.';\n\n  @override\n  String get backupPasswordWrong => 'Contraseña de respaldo incorrecta';\n\n  @override\n  String get backupTip =>\n      'Los datos exportados pueden ser encriptados con contraseña. \\nPor favor guárdalos en un lugar seguro.';\n\n  @override\n  String get backupVersionNotMatch =>\n      'La versión de la copia de seguridad no coincide, no se puede restaurar';\n\n  @override\n  String get bgRun => 'Ejecución en segundo plano';\n\n  @override\n  String get bgRunTip =>\n      'Este interruptor solo indica que la aplicación intentará correr en segundo plano, si puede hacerlo o no depende de si tiene el permiso correspondiente. En Android puro, por favor desactiva la “optimización de batería” para esta app, en MIUI por favor cambia la estrategia de ahorro de energía a “Sin restricciones”.';\n\n  @override\n  String get clearAllStatsContent =>\n      '¿Estás seguro de que quieres limpiar todas las estadísticas de conexión del servidor? Esta acción no se puede deshacer.';\n\n  @override\n  String get clearAllStatsTitle => 'Limpiar todas las estadísticas';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return '¿Estás seguro de que quieres limpiar las estadísticas de conexión del servidor \\\"$serverName\\\"? Esta acción no se puede deshacer.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return 'Limpiar estadísticas de $serverName';\n  }\n\n  @override\n  String get clearThisServerStats => 'Limpiar estadísticas de este servidor';\n\n  @override\n  String get compactDatabase => 'Compactar base de datos';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Tamaño de la base de datos: $size\\n\\nEsto reorganizará la base de datos para reducir el tamaño del archivo. No se eliminará ningún dato.';\n  }\n\n  @override\n  String get closeAfterSave => 'Guardar y cerrar';\n\n  @override\n  String get collapseUITip =>\n      '¿Colapsar por defecto las listas largas en la UI?';\n\n  @override\n  String get connectionDetails => 'Detalles de conexión';\n\n  @override\n  String get connectionStats => 'Estadísticas de conexión';\n\n  @override\n  String get connectionStatsDesc =>\n      'Ver la tasa de éxito de conexión del servidor e historial';\n\n  @override\n  String get containerTrySudoTip =>\n      'Por ejemplo: si configuras el usuario dentro de la app como aaa, pero Docker está instalado bajo el usuario root, entonces necesitarás habilitar esta opción';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Se requiere contraseña de sudo para acceder a Docker. Por favor ingrese su contraseña.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'La contraseña de sudo es incorrecta o no está permitida. Por favor intente de nuevo.';\n\n  @override\n  String get convert => 'Convertir';\n\n  @override\n  String get copyPath => 'Copiar ruta';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Muestre la tasa de uso de cada CPU en estilo de barra de progreso (estilo antiguo)';\n\n  @override\n  String get cursorType => 'Tipo de cursor';\n\n  @override\n  String get customCmd => 'Comandos personalizados';\n\n  @override\n  String get customCmdHint => '\\\"Nombre del comando\\\": \\\"Comando\\\"';\n\n  @override\n  String get deleteServers => 'Eliminar servidores en lote';\n\n  @override\n  String get desktopTerminalTip =>\n      'Comando utilizado para abrir el emulador de terminal al iniciar sesiones SSH.';\n\n  @override\n  String get dirEmpty => 'Asegúrate de que el directorio esté vacío';\n\n  @override\n  String get discoverSshServers => 'Descubrir servidores SSH';\n\n  @override\n  String get discoveryFailed => 'Falló el descubrimiento';\n\n  @override\n  String get discoverySettings => 'Configuración de descubrimiento';\n\n  @override\n  String get discoverySummary => 'Resumen del descubrimiento';\n\n  @override\n  String get diskHealth => 'Salud del disco';\n\n  @override\n  String get diskIgnorePath => 'Rutas de disco ignoradas';\n\n  @override\n  String get displayCpuIndex => 'Muestre el índice de CPU';\n\n  @override\n  String dl2Local(Object fileName) {\n    return '¿Descargar $fileName a local?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'No hay contenedores en ejecución.\\nEsto podría deberse a que:\\n- El usuario con el que se instaló Docker es diferente al configurado en la app\\n- La variable de entorno DOCKER_HOST no se ha leído correctamente. Puedes obtenerla ejecutando `echo \\$DOCKER_HOST` en el terminal.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return 'Total de $count imágenes';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker no está instalado';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount en ejecución, $stoppedCount detenidos';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count contenedores en ejecución';\n  }\n\n  @override\n  String get doubleColumnMode => 'Modo de doble columna';\n\n  @override\n  String get doubleColumnTip =>\n      'Esta opción solo habilita la función, si se puede activar o no depende del ancho del dispositivo';\n\n  @override\n  String get editVirtKeys => 'Editar teclas virtuales';\n\n  @override\n  String get editorHighlightTip =>\n      'El rendimiento del resaltado de código es bastante pobre actualmente, puedes elegir desactivarlo para mejorar.';\n\n  @override\n  String get enableMdns => 'Habilitar mDNS';\n\n  @override\n  String get enableMdnsDesc => 'Usar mDNS/Bonjour para descubrir servicios SSH';\n\n  @override\n  String get envVars => 'Variable de entorno';\n\n  @override\n  String get experimentalFeature => 'Función experimental';\n\n  @override\n  String get extraArgs => 'Argumentos extra';\n\n  @override\n  String get fallbackSshDest => 'Destino SSH alternativo';\n\n  @override\n  String get fdroidReleaseTip =>\n      'Si descargaste esta aplicación desde F-Droid, se recomienda desactivar esta opción.';\n\n  @override\n  String get fgService => 'Servicio en primer plano';\n\n  @override\n  String get fgServiceTip =>\n      'Después de activarlo, algunos modelos de dispositivos pueden bloquearse. Desactivarlo puede hacer que algunos modelos no puedan mantener las conexiones SSH en segundo plano. Por favor, permita los permisos de notificación de ServerBox, la ejecución en segundo plano y el auto-despertar en la configuración del sistema.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'El archivo \\'$file\\' es demasiado grande \\'$size\\', supera el $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Terminado en';\n\n  @override\n  String get followSystem => 'Seguir al sistema';\n\n  @override\n  String get fontSize => 'Tamaño de fuente';\n\n  @override\n  String get fullScreen => 'Modo pantalla completa';\n\n  @override\n  String get fullScreenJitter => 'Temblores en modo pantalla completa';\n\n  @override\n  String get fullScreenJitterHelp => 'Prevención de quemaduras de pantalla';\n\n  @override\n  String get fullScreenTip =>\n      '¿Debe habilitarse el modo de pantalla completa cuando el dispositivo se rote al modo horizontal? Esta opción solo se aplica a la pestaña del servidor.';\n\n  @override\n  String get goBackQ => '¿Regresar?';\n\n  @override\n  String get goto => 'Ir a';\n\n  @override\n  String get hideTitleBar => 'Ocultar barra de título';\n\n  @override\n  String get highlight => 'Resaltar código';\n\n  @override\n  String get homeTabs => 'Pestañas de inicio';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Personaliza qué pestañas aparecen en la página de inicio y su orden';\n\n  @override\n  String get homeWidgetUrlConfig => 'Configuración de URL del widget de inicio';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'Fallo en la solicitud, código de estado: $code';\n  }\n\n  @override\n  String get ignoreCert => 'Ignorar certificado';\n\n  @override\n  String get image => 'Imagen';\n\n  @override\n  String get imagesList => 'Lista de imágenes';\n\n  @override\n  String get installDockerWithUrl =>\n      'Por favor instala Docker primero desde https://docs.docker.com/engine/install';\n\n  @override\n  String get invalid => 'Inválido';\n\n  @override\n  String get invalidHostFormat =>\n      'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.';\n\n  @override\n  String get jumpServer => 'Servidor de salto';\n\n  @override\n  String get keepForeground => '¡Por favor, mantén la app en primer plano!';\n\n  @override\n  String get keepStatusWhenErr => 'Mantener el estado anterior del servidor';\n\n  @override\n  String get keepStatusWhenErrTip =>\n      'Solo aplica cuando hay errores al ejecutar scripts';\n\n  @override\n  String get keyAuth => 'Autenticación con llave';\n\n  @override\n  String get lastFailure => 'Último fallo';\n\n  @override\n  String get lastSuccess => 'Último éxito';\n\n  @override\n  String get letterCache => 'Caché de letras';\n\n  @override\n  String get letterCacheTip =>\n      'Recomendado desactivar, pero después de desactivarlo, no se podrán ingresar caracteres CJK.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return 'Hecho con ❤️ por $myGithub';\n  }\n\n  @override\n  String get max => 'Máximo';\n\n  @override\n  String get maxConcurrency => 'Concurrencia máxima';\n\n  @override\n  String get maxRetryCount =>\n      'Número máximo de reintentos de conexión al servidor';\n\n  @override\n  String get maxRetryCountEqual0 => 'Reintentará infinitamente';\n\n  @override\n  String get min => 'Mínimo';\n\n  @override\n  String get more => 'Más';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'Activado: se mostrará debajo de cada tarjeta en la página de servidores. Desactivado: se mostrará en la parte superior de los detalles del servidor.';\n\n  @override\n  String get needHomeDir =>\n      'Si eres usuario de Synology, [consulta aquí](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Los usuarios de otros sistemas deben buscar cómo crear un directorio home.';\n\n  @override\n  String get needRestart => 'Necesita reiniciar la app';\n\n  @override\n  String get netViewType => 'Tipo de vista de red';\n\n  @override\n  String get newContainer => 'Crear contenedor nuevo';\n\n  @override\n  String get noConnectionStatsData =>\n      'No hay datos de estadísticas de conexión';\n\n  @override\n  String get noLineChart => 'No utilice gráficos de líneas';\n\n  @override\n  String get noLineChartForCpu => 'No utilice gráficos lineales para la CPU';\n\n  @override\n  String get noPrivateKeyTip =>\n      'La clave privada no existe, puede haber sido eliminada o hay un error de configuración.';\n\n  @override\n  String get noPromptAgain => 'No volver a preguntar';\n\n  @override\n  String get onServerDetailPage => 'En la página de detalles del servidor';\n\n  @override\n  String get onlyOneLine => 'Mostrar solo en una línea (desplazable)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 =>\n      'Efectivo solo cuando el número de núcleos > 8';\n\n  @override\n  String get openLastPath => 'Abrir el último camino';\n\n  @override\n  String get openLastPathTip =>\n      'Los diferentes servidores tendrán diferentes registros, y lo que se registra es la ruta de salida';\n\n  @override\n  String get parseContainerStatsTip =>\n      'El análisis del estado de uso de Docker es bastante lento';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return 'El $percent% de $size';\n  }\n\n  @override\n  String get permission => 'Permisos';\n\n  @override\n  String get pingInputIP =>\n      'Por favor, introduce la IP de destino o el dominio';\n\n  @override\n  String get pingNoServer =>\n      'No hay servidores disponibles para hacer Ping\\nPor favor, añade un servidor en la pestaña de servidores y vuelve a intentarlo';\n\n  @override\n  String get plugInType => 'Tipo de inserción';\n\n  @override\n  String get preferDiskAmount =>\n      'Priorizar la visualización de la capacidad del disco';\n\n  @override\n  String get privateKey => 'Llave privada';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'No se encontró la clave privada [$keyId].';\n  }\n\n  @override\n  String get pushToken => 'Token de notificaciones';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'No se recomienda activarlo, ¡tenga cuidado con los riesgos de seguridad! Si está utilizando el certificado predeterminado de PVE, debe habilitar esta opción.';\n\n  @override\n  String get pveLoginFailed =>\n      'Fallo al iniciar sesión. No se puede autenticar con el nombre de usuario/contraseña de la configuración del servidor para el inicio de sesión de Linux PAM.';\n\n  @override\n  String get pveVersionLow =>\n      'Esta función está actualmente en fase de prueba y solo se ha probado en PVE 8+. Úsela con precaución.';\n\n  @override\n  String get read => 'Leer';\n\n  @override\n  String get recentConnections => 'Conexiones recientes';\n\n  @override\n  String get rememberPwdInMem => 'Recordar contraseña en la memoria';\n\n  @override\n  String get rememberPwdInMemTip =>\n      'Utilizado para contenedores, suspensión, etc.';\n\n  @override\n  String get rememberWindowSize => 'Recordar el tamaño de la ventana';\n\n  @override\n  String get remotePath => 'Ruta remota';\n\n  @override\n  String get result => 'Resultado';\n\n  @override\n  String get rotateAngel => 'Ángulo de rotación';\n\n  @override\n  String get sameIdServerExist => 'Ya existe un servidor con el mismo ID';\n\n  @override\n  String get save => 'Guardar';\n\n  @override\n  String get second => 'Segundo';\n\n  @override\n  String get serverDetailOrder =>\n      'Orden de los componentes en la página de detalles del servidor';\n\n  @override\n  String get serverFuncBtns => 'Botones de función del servidor';\n\n  @override\n  String get serverOrder => 'Orden del servidor';\n\n  @override\n  String get serverTabRequired =>\n      'La pestaña del servidor no se puede eliminar';\n\n  @override\n  String get sftpDlPrepare => 'Preparando para conectar al servidor...';\n\n  @override\n  String get sftpEditorTip =>\n      'Si está vacío, use el editor de archivos incorporado de la aplicación. Si hay un valor, use el editor del servidor remoto, por ejemplo, `vim` (se recomienda detectar automáticamente según `EDITOR`).';\n\n  @override\n  String get sftpRmrDirSummary =>\n      'Usar `rm -r` en SFTP para eliminar directorios';\n\n  @override\n  String get sftpSSHConnected => 'SFTP conectado...';\n\n  @override\n  String get sftpShowFoldersFirst => 'Mostrar carpetas primero';\n\n  @override\n  String get showDistLogo => 'Mostrar logo de distribución';\n\n  @override\n  String get size => 'Tamaño';\n\n  @override\n  String get softWrap => 'Salto de línea suave';\n\n  @override\n  String get specifyDev => 'Especificar dispositivo';\n\n  @override\n  String get specifyDevTip =>\n      'Por ejemplo, las estadísticas de tráfico de red son por defecto para todos los dispositivos. Aquí puede especificar un dispositivo en particular.';\n\n  @override\n  String get speed => 'Velocidad';\n\n  @override\n  String spentTime(Object time) {\n    return 'Tiempo gastado: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'Todos los servidores ya existen (se encontraron $duplicateCount duplicados)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return 'Se omitirán $duplicateCount duplicados';\n  }\n\n  @override\n  String get sshConfigFound => 'Encontramos configuración SSH en tu sistema';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return 'Se encontraron $totalCount servidores';\n  }\n\n  @override\n  String get sshConfigImport => 'Importar Configuración SSH';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Solo se pueden importar datos básicos, por ejemplo: IP/Puerto.';\n\n  @override\n  String get sshConfigImportPermission =>\n      '¿Te gustaría dar permiso para leer ~/.ssh/config e importar automáticamente la configuración de servidores?';\n\n  @override\n  String get sshConfigImportTip =>\n      'Sugerencia para leer ~/.ssh/config al crear el primer servidor';\n\n  @override\n  String sshConfigImported(Object count) {\n    return 'Se importaron $count servidores desde la configuración SSH';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return 'La clave de host SSH de $serverName ha cambiado. Continúa solo si confías en este servidor.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Huella (MD5 Base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Huella (MD5 hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'Tipo de clave de host SSH';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return 'Se recibió una nueva clave de host SSH de $serverName. Revisa la huella antes de confiar.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Huella almacenada: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      '¿Te gustaría seleccionar manualmente el archivo de configuración SSH?';\n\n  @override\n  String get sshConfigNoServers =>\n      'No se encontraron servidores en la configuración SSH';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'No se puede acceder al archivo de configuración SSH debido a los permisos de macOS.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return 'Se importarán $importCount servidores';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'Cuando el terminal es desplazable, arrastrar horizontalmente puede seleccionar texto. Hacer clic en el botón del teclado enciende/apaga el teclado. El icono de archivo abre el SFTP de la ruta actual. El botón del portapapeles copia el contenido cuando se selecciona texto y pega el contenido del portapapeles en el terminal cuando no se selecciona texto y hay contenido en el portapapeles. El icono de código pega fragmentos de código en el terminal y los ejecuta.';\n\n  @override\n  String sshTip(Object url) {\n    return 'Esta función está en fase de pruebas.\\n\\nPor favor, informa los problemas en $url, o únete a nuestro desarrollo.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff =>\n      'Desactivación automática de teclas virtuales';\n\n  @override\n  String get stat => 'Estadísticas';\n\n  @override\n  String get supportFmtArgs => 'Soporta los siguientes argumentos de formato:';\n\n  @override\n  String get suspendTip =>\n      'La función de suspender necesita permisos de root y soporte de systemd.';\n\n  @override\n  String switchTo(Object val) {\n    return 'Cambiar a $val';\n  }\n\n  @override\n  String get syncTip =>\n      'Puede que necesites reiniciar para que algunos cambios tengan efecto.';\n\n  @override\n  String get system => 'Sistema';\n\n  @override\n  String get tag => 'Etiqueta';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Toca el botón de búsqueda para descubrir servidores SSH en tu red';\n\n  @override\n  String get termFontSizeTip =>\n      'Este ajuste afectará el tamaño del terminal (ancho y alto). Puedes hacer zoom en la página del terminal para ajustar el tamaño de fuente de la sesión actual.';\n\n  @override\n  String get textScaler => 'Escalar texto';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (tamaño original), solo afecta a ciertas fuentes en la página del servidor, no se recomienda modificar.';\n\n  @override\n  String get time => 'Tiempo';\n\n  @override\n  String get times => 'Veces';\n\n  @override\n  String get trySudo => 'Intentar con sudo';\n\n  @override\n  String get unknown => 'Desconocido';\n\n  @override\n  String get unkownConvertMode => 'Modo de conversión desconocido';\n\n  @override\n  String get update => 'Actualizar';\n\n  @override\n  String get updateIntervalEqual0 =>\n      'Si configuras esto a 0, el estado del servidor no se refrescará automáticamente.\\nY no se podrá calcular el uso de CPU.';\n\n  @override\n  String get updateServerStatusInterval =>\n      'Intervalo de actualización del estado del servidor';\n\n  @override\n  String get upsideDown => 'Invertir arriba por abajo';\n\n  @override\n  String get useCdn => 'Usando CDN';\n\n  @override\n  String get useCdnTip =>\n      'Se recomienda a los usuarios no chinos utilizar CDN. ¿Le gustaría utilizarlo?';\n\n  @override\n  String get useNoPwd => 'Se usará sin contraseña';\n\n  @override\n  String get usePodmanByDefault => 'Usar Podman por defecto';\n\n  @override\n  String get used => 'Usado';\n\n  @override\n  String get view => 'Vista';\n\n  @override\n  String get viewDetails => 'Ver detalles';\n\n  @override\n  String get viewErr => 'Ver error';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'Si el terminal tiene caracteres seleccionados, entonces copiará los caracteres seleccionados al portapapeles, de lo contrario, pegará el contenido del portapapeles al terminal.';\n\n  @override\n  String get virtKeyHelpIME => 'Encender/apagar el teclado';\n\n  @override\n  String get virtKeyHelpSFTP => 'Abrir la ruta actual en SFTP.';\n\n  @override\n  String get waitConnection =>\n      'Por favor, espera a que la conexión se establezca';\n\n  @override\n  String get wakeLock => 'Mantener despierto';\n\n  @override\n  String get watchNotPaired => 'No hay un Apple Watch emparejado';\n\n  @override\n  String get webdavSettingEmpty => 'La configuración de Webdav está vacía';\n\n  @override\n  String get whenOpenApp => 'Al abrir la App';\n\n  @override\n  String get wolTip =>\n      'Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.';\n\n  @override\n  String get write => 'Escribir';\n\n  @override\n  String get writeScriptFailTip =>\n      'La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.';\n\n  @override\n  String get writeScriptTip =>\n      'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \\n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Detectada emulación de Podman Docker. Por favor, cambie a Podman en la configuración.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_fr.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for French (`fr`).\nclass AppLocalizationsFr extends AppLocalizations {\n  AppLocalizationsFr([String locale = 'fr']) : super(locale);\n\n  @override\n  String get aboutThanks => 'Merci aux personnes suivantes qui ont participé.';\n\n  @override\n  String get acceptBeta => 'Accepter les mises à jour de la version de test';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'Actuellement, vous n\\'avez aucune clé privée. Souhaitez-vous ajouter celle qui vient avec le système (~/.ssh/id_rsa) ?';\n\n  @override\n  String get added2List => 'Ajouté à la liste des tâches';\n\n  @override\n  String get addr => 'Adresse';\n\n  @override\n  String get alreadyLastDir => 'Déjà dans le dernier répertoire.';\n\n  @override\n  String get askAi => 'Demander à l\\'IA';\n\n  @override\n  String get askAiApiKey => 'Clé API';\n\n  @override\n  String get askAiAwaitingResponse => 'En attente de la réponse de l\\'IA...';\n\n  @override\n  String get askAiBaseUrl => 'URL de base';\n\n  @override\n  String get askAiCommandInserted => 'Commande insérée dans le terminal';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Veuillez configurer $fields dans les paramètres.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Confirmer avant d\\'exécuter';\n\n  @override\n  String get askAiConversation => 'Conversation avec l\\'IA';\n\n  @override\n  String get askAiDisclaimer =>\n      'L\\'IA peut se tromper. Utilisez-la avec prudence.';\n\n  @override\n  String get askAiFollowUpHint => 'Poser une question supplémentaire...';\n\n  @override\n  String get askAiInsertTerminal => 'Insérer dans le terminal';\n\n  @override\n  String get askAiNoResponse => 'Aucune réponse';\n\n  @override\n  String get askAiRecommendedCommand => 'Commande suggérée par l\\'IA';\n\n  @override\n  String get askAiSelectedContent => 'Contenu sélectionné';\n\n  @override\n  String get askAiUsageHint => 'Utilisé dans le terminal SSH';\n\n  @override\n  String get atLeastOneTab => 'Au moins un onglet doit être sélectionné';\n\n  @override\n  String get authFailTip =>\n      'Échec de l\\'authentification. Veuillez vérifier si le mot de passe/clé/hôte/utilisateur, etc., est incorrect.';\n\n  @override\n  String get autoBackupConflict =>\n      'Un seul sauvegarde automatique peut être activé en même temps.';\n\n  @override\n  String get autoConnect => 'Connexion automatique';\n\n  @override\n  String get autoRun => 'Exécution automatique';\n\n  @override\n  String get autoUpdateHomeWidget =>\n      'Mise à jour automatique du widget d\\'accueil';\n\n  @override\n  String get availableTabs => 'Onglets disponibles';\n\n  @override\n  String get backupEncrypted => 'La sauvegarde est chiffrée';\n\n  @override\n  String get backupNotEncrypted => 'La sauvegarde n\\'est pas chiffrée';\n\n  @override\n  String get backupPassword => 'Mot de passe de sauvegarde';\n\n  @override\n  String get backupPasswordRemoved => 'Mot de passe de sauvegarde supprimé';\n\n  @override\n  String get backupPasswordSet => 'Mot de passe de sauvegarde défini';\n\n  @override\n  String get backupPasswordTip =>\n      'Définissez un mot de passe pour chiffrer les fichiers de sauvegarde. Laissez vide pour désactiver le chiffrement.';\n\n  @override\n  String get backupPasswordWrong => 'Mot de passe de sauvegarde incorrect';\n\n  @override\n  String get backupTip =>\n      'Les données exportées peuvent être chiffrées avec un mot de passe. \\nVeuillez les garder en sécurité.';\n\n  @override\n  String get backupVersionNotMatch =>\n      'La version de sauvegarde ne correspond pas.';\n\n  @override\n  String get bgRun => 'Exécution en arrière-plan';\n\n  @override\n  String get bgRunTip =>\n      'Cette option signifie seulement que le programme essaiera de s\\'exécuter en arrière-plan, que cela soit possible dépend de l\\'autorisation activée ou non. Pour Android natif, veuillez désactiver l\\'« Optimisation de la batterie » dans cette application, et pour MIUI, veuillez changer la politique d\\'économie d\\'énergie en « Illimité ».';\n\n  @override\n  String get clearAllStatsContent =>\n      'Êtes-vous sûr de vouloir effacer toutes les statistiques de connexion des serveurs ? Cette action ne peut pas être annulée.';\n\n  @override\n  String get clearAllStatsTitle => 'Effacer toutes les statistiques';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return 'Êtes-vous sûr de vouloir effacer les statistiques de connexion du serveur \\\"$serverName\\\" ? Cette action ne peut pas être annulée.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return 'Effacer les statistiques de $serverName';\n  }\n\n  @override\n  String get clearThisServerStats => 'Effacer les statistiques de ce serveur';\n\n  @override\n  String get compactDatabase => 'Compacter la base de données';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Taille de la base de données : $size\\n\\nCela réorganisera la base de données pour réduire la taille du fichier. Aucune donnée ne sera supprimée.';\n  }\n\n  @override\n  String get closeAfterSave => 'Enregistrer et fermer';\n\n  @override\n  String get collapseUITip =>\n      'Indique si les longues listes présentées dans l\\'interface utilisateur doivent être réduites par défaut.';\n\n  @override\n  String get connectionDetails => 'Détails de connexion';\n\n  @override\n  String get connectionStats => 'Statistiques de connexion';\n\n  @override\n  String get connectionStatsDesc =>\n      'Voir le taux de réussite de connexion du serveur et l\\'historique';\n\n  @override\n  String get containerTrySudoTip =>\n      'Par exemple : Dans l\\'application, l\\'utilisateur est défini comme aaa, mais Docker est installé sous l\\'utilisateur root. Dans ce cas, vous devez activer cette option.';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Un mot de passe sudo est requis pour accéder à Docker. Veuillez entrer votre mot de passe.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'Le mot de passe sudo est incorrect ou non autorisé. Veuillez réessayer.';\n\n  @override\n  String get convert => 'Convertir';\n\n  @override\n  String get copyPath => 'Copier le chemin';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Afficher le taux d\\'utilisation de chaque CPU sous forme de barre de progression (ancien style)';\n\n  @override\n  String get cursorType => 'Type de curseur';\n\n  @override\n  String get customCmd => 'Commandes personnalisées';\n\n  @override\n  String get customCmdHint => '\\\"Nom de la commande\\\": \\\"Commande\\\"';\n\n  @override\n  String get deleteServers => 'Supprimer des serveurs en lot';\n\n  @override\n  String get desktopTerminalTip =>\n      'Commande utilisée pour ouvrir l’émulateur de terminal lors du lancement de sessions SSH.';\n\n  @override\n  String get dirEmpty => 'Assurez-vous que le répertoire est vide.';\n\n  @override\n  String get discoverSshServers => 'Découvrir les serveurs SSH';\n\n  @override\n  String get discoveryFailed => 'Échec de la découverte';\n\n  @override\n  String get discoverySettings => 'Paramètres de découverte';\n\n  @override\n  String get discoverySummary => 'Résumé de la découverte';\n\n  @override\n  String get diskHealth => 'Santé du disque';\n\n  @override\n  String get diskIgnorePath => 'Chemin à ignorer pour le disque';\n\n  @override\n  String get displayCpuIndex => 'Afficher l\\'index CPU';\n\n  @override\n  String dl2Local(Object fileName) {\n    return 'Télécharger $fileName localement ?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'Aucun conteneur en cours d\\'exécution.\\nCela peut être dû à :\\n- L\\'utilisateur d\\'installation de Docker n\\'est pas le même que celui configuré dans l\\'application.\\n- La variable d\\'environnement DOCKER_HOST n\\'a pas été lue correctement. Vous pouvez l\\'obtenir en exécutant `echo \\$DOCKER_HOST` dans le terminal.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '$count images';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker non installé';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount en cours d\\'exécution, $stoppedCount conteneur arrêté.';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count conteneur en cours d\\'exécution.';\n  }\n\n  @override\n  String get doubleColumnMode => 'Mode double colonne';\n\n  @override\n  String get doubleColumnTip =>\n      'Cette option n\\'active que la fonctionnalité, qu\\'elle puisse être activée dépend de la largeur de l\\'appareil.';\n\n  @override\n  String get editVirtKeys => 'Modifier les touches virtuelles';\n\n  @override\n  String get editorHighlightTip =>\n      'La performance actuelle de mise en surbrillance du code est pire et peut être désactivée en option pour s\\'améliorer.';\n\n  @override\n  String get enableMdns => 'Activer mDNS';\n\n  @override\n  String get enableMdnsDesc =>\n      'Utiliser mDNS/Bonjour pour découvrir les services SSH';\n\n  @override\n  String get envVars => 'Variable d’environnement';\n\n  @override\n  String get experimentalFeature => 'Fonctionnalité expérimentale';\n\n  @override\n  String get extraArgs => 'Arguments supplémentaires';\n\n  @override\n  String get fallbackSshDest => 'Destino SSH alternativo';\n\n  @override\n  String get fdroidReleaseTip =>\n      'Si vous avez téléchargé cette application depuis F-Droid, il est recommandé de désactiver cette option.';\n\n  @override\n  String get fgService => 'Service de premier plan';\n\n  @override\n  String get fgServiceTip =>\n      'Après l\\'activation, certains modèles d\\'appareils peuvent planter. La désactivation peut empêcher certains modèles de maintenir les connexions SSH en arrière-plan. Veuillez autoriser les permissions de notification ServerBox, l\\'exécution en arrière-plan et l\\'auto-réveil dans les paramètres système.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'Fichier \\'$file\\' trop volumineux $size, max $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Terminé à';\n\n  @override\n  String get followSystem => 'Suivre le système';\n\n  @override\n  String get fontSize => 'Taille de la police';\n\n  @override\n  String get fullScreen => 'Mode plein écran';\n\n  @override\n  String get fullScreenJitter => 'Secousse en plein écran';\n\n  @override\n  String get fullScreenJitterHelp => 'Pour éviter les brûlures d\\'écran';\n\n  @override\n  String get fullScreenTip =>\n      'Le mode plein écran doit-il être activé lorsque l\\'appareil est orienté en mode paysage ? Cette option s\\'applique uniquement à l\\'onglet serveur.';\n\n  @override\n  String get goBackQ => 'Revenir en arrière ?';\n\n  @override\n  String get goto => 'Aller à';\n\n  @override\n  String get hideTitleBar => 'Masquer la barre de titre';\n\n  @override\n  String get highlight => 'Mise en surbrillance du code';\n\n  @override\n  String get homeTabs => 'Onglets d\\'accueil';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Personnalisez les onglets qui apparaissent sur la page d\\'accueil et leur ordre';\n\n  @override\n  String get homeWidgetUrlConfig => 'Configurer l\\'URL du widget d\\'accueil';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'Échec de la requête, code d\\'état : $code';\n  }\n\n  @override\n  String get ignoreCert => 'Ignorer le certificat';\n\n  @override\n  String get image => 'Image';\n\n  @override\n  String get imagesList => 'Liste des images';\n\n  @override\n  String get installDockerWithUrl =>\n      'Veuillez d\\'abord installer docker depuis https://docs.docker.com/engine/install.';\n\n  @override\n  String get invalid => 'Invalide';\n\n  @override\n  String get invalidHostFormat =>\n      'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.';\n\n  @override\n  String get jumpServer => 'Aller au serveur';\n\n  @override\n  String get keepForeground => 'Garder l\\'application en premier plan !';\n\n  @override\n  String get keepStatusWhenErr => 'Conserver l\\'état du dernier serveur';\n\n  @override\n  String get keepStatusWhenErrTip =>\n      'Uniquement en cas d\\'erreur lors de l\\'exécution du script';\n\n  @override\n  String get keyAuth => 'Authentification par clé';\n\n  @override\n  String get lastFailure => 'Dernier échec';\n\n  @override\n  String get lastSuccess => 'Dernier succès';\n\n  @override\n  String get letterCache => 'Mise en cache des lettres';\n\n  @override\n  String get letterCacheTip =>\n      'Recommandé de désactiver, mais après désactivation, il sera impossible de saisir des caractères CJK.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return 'Fabriqué avec ❤️ par $myGithub';\n  }\n\n  @override\n  String get max => 'max';\n\n  @override\n  String get maxConcurrency => 'Concurrence maximale';\n\n  @override\n  String get maxRetryCount => 'Nombre de reconnexions au serveur';\n\n  @override\n  String get maxRetryCountEqual0 => 'Il va réessayer encore et encore.';\n\n  @override\n  String get min => 'min';\n\n  @override\n  String get more => 'Plus';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'Activé : peut être affiché sous chaque carte sur la page de l\\'onglet Serveur. Désactivé : peut être affiché en haut de la page de détails du serveur.';\n\n  @override\n  String get needHomeDir =>\n      'Si vous êtes utilisateur Synology, [consultez ici](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Les utilisateurs d\\'autres systèmes doivent rechercher comment créer un répertoire personnel.';\n\n  @override\n  String get needRestart => 'Nécessite un redémarrage de l\\'application';\n\n  @override\n  String get netViewType => 'Type de vue réseau';\n\n  @override\n  String get newContainer => 'Nouveau conteneur';\n\n  @override\n  String get noConnectionStatsData =>\n      'Aucune donnée de statistiques de connexion';\n\n  @override\n  String get noLineChart => 'Ne pas utiliser de graphiques linéaires';\n\n  @override\n  String get noLineChartForCpu =>\n      'Ne pas utiliser de graphiques linéaires pour l\\'unité centrale';\n\n  @override\n  String get noPrivateKeyTip =>\n      'La clé privée n\\'existe pas, elle a peut-être été supprimée ou il y a une erreur de configuration.';\n\n  @override\n  String get noPromptAgain => 'Ne pas demander à nouveau';\n\n  @override\n  String get onServerDetailPage => 'Sur la page de détails du serveur';\n\n  @override\n  String get onlyOneLine =>\n      'Afficher uniquement en une seule ligne (défilement)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 =>\n      'Fonctionne uniquement lorsque le nombre de cœurs est > 8';\n\n  @override\n  String get openLastPath => 'Ouvrir le dernier chemin';\n\n  @override\n  String get openLastPathTip =>\n      'Les différents serveurs auront des journaux différents, et le journal est le chemin vers la sortie';\n\n  @override\n  String get parseContainerStatsTip =>\n      'L\\'analyse de l\\'occupation des conteneurs Docker est relativement lente.';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$percent% de $size';\n  }\n\n  @override\n  String get permission => 'Permissions';\n\n  @override\n  String get pingInputIP =>\n      'Veuillez saisir une adresse IP / un domaine cible.';\n\n  @override\n  String get pingNoServer =>\n      'Aucun serveur à pinger.\\nVeuillez ajouter un serveur dans l\\'onglet serveur.';\n\n  @override\n  String get plugInType => 'Type d\\'insertion';\n\n  @override\n  String get preferDiskAmount =>\n      'Prioriser l’affichage de la capacité du disque';\n\n  @override\n  String get privateKey => 'Clé privée';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'Clé privée [$keyId] introuvable.';\n  }\n\n  @override\n  String get pushToken => 'Jeton d\\'identification';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'Il n\\'est pas recommandé de l\\'activer, attention aux risques de sécurité ! Si vous utilisez le certificat par défaut de PVE, vous devez activer cette option.';\n\n  @override\n  String get pveLoginFailed =>\n      'Échec de la connexion. Impossible d\\'authentifier avec le nom d\\'utilisateur / mot de passe de la configuration du serveur pour la connexion Linux PAM.';\n\n  @override\n  String get pveVersionLow =>\n      'Cette fonctionnalité est actuellement en phase de test et n\\'a été testée que sur PVE 8+. Veuillez l\\'utiliser avec prudence.';\n\n  @override\n  String get read => 'Lire';\n\n  @override\n  String get recentConnections => 'Connexions récentes';\n\n  @override\n  String get rememberPwdInMem => 'Mémoriser le mot de passe en mémoire';\n\n  @override\n  String get rememberPwdInMemTip =>\n      'Utilisé pour les conteneurs, la suspension, etc.';\n\n  @override\n  String get rememberWindowSize => 'Se souvenir de la taille de la fenêtre';\n\n  @override\n  String get remotePath => 'Chemin distant';\n\n  @override\n  String get result => 'Résultat';\n\n  @override\n  String get rotateAngel => 'Angle de rotation';\n\n  @override\n  String get sameIdServerExist => 'Un serveur avec le même ID existe déjà';\n\n  @override\n  String get save => 'Enregistrer';\n\n  @override\n  String get second => 's';\n\n  @override\n  String get serverDetailOrder =>\n      'Ordre des widgets de la page de détails du serveur';\n\n  @override\n  String get serverFuncBtns => 'Boutons de fonction du serveur';\n\n  @override\n  String get serverOrder => 'Ordre du serveur';\n\n  @override\n  String get serverTabRequired => 'L\\'onglet serveur ne peut pas être supprimé';\n\n  @override\n  String get sftpDlPrepare => 'Préparation de la connexion...';\n\n  @override\n  String get sftpEditorTip =>\n      'Si vide, utilisez l’éditeur de fichiers intégré de l’application. Si une valeur est présente, utilisez l’éditeur du serveur distant, par exemple `vim` (il est recommandé de détecter automatiquement selon `EDITOR`).';\n\n  @override\n  String get sftpRmrDirSummary =>\n      'Utilisez `rm -r` pour supprimer un dossier en SFTP.';\n\n  @override\n  String get sftpSSHConnected => 'SFTP Connecté';\n\n  @override\n  String get sftpShowFoldersFirst => 'Afficher d\\'abord les dossiers';\n\n  @override\n  String get showDistLogo => 'Afficher le logo de la distribution';\n\n  @override\n  String get size => 'Taille';\n\n  @override\n  String get softWrap => 'Retour à la ligne souple';\n\n  @override\n  String get specifyDev => 'Spécifier l\\'appareil';\n\n  @override\n  String get specifyDevTip =>\n      'Par exemple, les statistiques de trafic réseau concernent par défaut tous les appareils. Vous pouvez spécifier ici un appareil particulier.';\n\n  @override\n  String get speed => 'Vitesse';\n\n  @override\n  String spentTime(Object time) {\n    return 'Temps écoulé : $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'Tous les serveurs existent déjà ($duplicateCount doublons trouvés)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount doublons seront ignorés';\n  }\n\n  @override\n  String get sshConfigFound =>\n      'Nous avons trouvé une configuration SSH sur votre système.';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return '$totalCount serveurs trouvés';\n  }\n\n  @override\n  String get sshConfigImport => 'Importation de configuration SSH';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Seules les informations de base peuvent être importées, par exemple : IP/Port.';\n\n  @override\n  String get sshConfigImportPermission =>\n      'Souhaitez-vous donner la permission de lire ~/.ssh/config et d\\'importer automatiquement les paramètres du serveur ?';\n\n  @override\n  String get sshConfigImportTip =>\n      'Proposer de lire ~/.ssh/config lors de la première création de serveur';\n\n  @override\n  String sshConfigImported(Object count) {\n    return '$count serveurs importés depuis la configuration SSH';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return 'La clé d\\'hôte SSH de $serverName a changé. Ne continuez que si vous faites confiance à ce serveur.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Empreinte (MD5 Base64) : $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Empreinte (MD5 hex) : $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'Type de clé d\\'hôte SSH';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return 'Une nouvelle clé d\\'hôte SSH a été reçue de $serverName. Vérifiez l\\'empreinte avant de faire confiance.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Empreinte enregistrée : $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      'Souhaitez-vous sélectionner manuellement le fichier de configuration SSH ?';\n\n  @override\n  String get sshConfigNoServers =>\n      'Aucun serveur trouvé dans la configuration SSH';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'Impossible d\\'accéder au fichier de configuration SSH en raison des permissions macOS.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount serveurs seront importés';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'Lorsque le terminal est défilable, faire glisser horizontalement permet de sélectionner du texte. En cliquant sur le bouton du clavier, vous activez/désactivez le clavier. L\\'icône de fichier ouvre le chemin actuel SFTP. Le bouton du presse-papiers copie le contenu lorsque du texte est sélectionné, et colle le contenu du presse-papiers dans le terminal lorsqu\\'aucun texte n\\'est sélectionné et qu\\'il y a du contenu dans le presse-papiers. L\\'icône de code colle des extraits de code dans le terminal et les exécute.';\n\n  @override\n  String sshTip(Object url) {\n    return 'Cette fonctionnalité est actuellement à l\\'étape expérimentale.\\n\\nVeuillez signaler les bugs sur $url ou rejoindre notre développement.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff =>\n      'Activation automatique des touches virtuelles';\n\n  @override\n  String get stat => 'Statistiques';\n\n  @override\n  String get supportFmtArgs =>\n      'Les paramètres de mise en forme suivants sont pris en charge :';\n\n  @override\n  String get suspendTip =>\n      'La fonction de suspension nécessite des privilèges root et le support de systemd.';\n\n  @override\n  String switchTo(Object val) {\n    return 'Passer à $val';\n  }\n\n  @override\n  String get syncTip =>\n      'Un redémarrage peut être nécessaire pour que certains changements prennent effet.';\n\n  @override\n  String get system => 'Système';\n\n  @override\n  String get tag => 'Étiquettes';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Appuyez sur le bouton de recherche pour découvrir les serveurs SSH sur votre réseau';\n\n  @override\n  String get termFontSizeTip =>\n      'Ce paramètre affectera la taille du terminal (largeur et hauteur). Vous pouvez zoomer sur la page du terminal pour ajuster la taille de la police de la session en cours.';\n\n  @override\n  String get textScaler => 'Mise à l\\'échelle du texte';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (taille originale), fonctionne uniquement sur la partie de la police de la page du serveur, il est déconseillé de la modifier.';\n\n  @override\n  String get time => 'Temps';\n\n  @override\n  String get times => 'Fois';\n\n  @override\n  String get trySudo => 'Essayer d\\'utiliser sudo';\n\n  @override\n  String get unknown => 'Inconnu';\n\n  @override\n  String get unkownConvertMode => 'Mode de conversion inconnu';\n\n  @override\n  String get update => 'Mettre à jour';\n\n  @override\n  String get updateIntervalEqual0 =>\n      'Vous avez défini à 0, la mise à jour ne se fera pas automatiquement.\\nImpossible de calculer l\\'état du CPU.';\n\n  @override\n  String get updateServerStatusInterval =>\n      'Intervalle de mise à jour de l\\'état du serveur';\n\n  @override\n  String get upsideDown => 'À l\\'envers';\n\n  @override\n  String get useCdn => 'Utiliser CDN';\n\n  @override\n  String get useCdnTip =>\n      'Il est recommandé aux utilisateurs non chinois d\\'utiliser le CDN. Souhaitez-vous l\\'utiliser ?';\n\n  @override\n  String get useNoPwd => 'Aucun mot de passe ne sera utilisé';\n\n  @override\n  String get usePodmanByDefault => 'Par défaut avec Podman';\n\n  @override\n  String get used => 'Utilisé';\n\n  @override\n  String get view => 'Vue';\n\n  @override\n  String get viewDetails => 'Voir les détails';\n\n  @override\n  String get viewErr => 'Voir erreur';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'Copiez dans le presse-papiers si le terminal sélectionné n\\'est pas vide, sinon collez le contenu du presse-papiers dans le terminal.';\n\n  @override\n  String get virtKeyHelpIME => 'Activer/désactiver le clavier';\n\n  @override\n  String get virtKeyHelpSFTP => 'Ouvrir le répertoire actuel en SFTP.';\n\n  @override\n  String get waitConnection =>\n      'Veuillez attendre que la connexion soit établie.';\n\n  @override\n  String get wakeLock => 'Maintenir éveillé';\n\n  @override\n  String get watchNotPaired => 'Aucune Apple Watch associée';\n\n  @override\n  String get webdavSettingEmpty => 'Le paramètre Webdav est vide';\n\n  @override\n  String get whenOpenApp => 'À l\\'ouverture de l\\'application';\n\n  @override\n  String get wolTip =>\n      'Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.';\n\n  @override\n  String get write => 'Écrire';\n\n  @override\n  String get writeScriptFailTip =>\n      'Échec de l\\'écriture dans le script, probablement en raison d\\'un manque de permissions ou que le répertoire n\\'existe pas.';\n\n  @override\n  String get writeScriptTip =>\n      'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \\n | `/tmp/server_box` pour surveiller l\\'état du système. Vous pouvez examiner le contenu du script.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Émulation Podman Docker détectée. Veuillez passer à Podman dans les paramètres.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_id.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Indonesian (`id`).\nclass AppLocalizationsId extends AppLocalizations {\n  AppLocalizationsId([String locale = 'id']) : super(locale);\n\n  @override\n  String get aboutThanks =>\n      'Terima kasih kepada orang -orang berikut yang berpartisipasi.';\n\n  @override\n  String get acceptBeta => 'Terima pembaruan versi uji coba';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?';\n\n  @override\n  String get added2List => 'Ditambahkan ke Daftar Tugas';\n\n  @override\n  String get addr => 'Alamat';\n\n  @override\n  String get alreadyLastDir => 'Sudah di direktori terakhir.';\n\n  @override\n  String get askAi => 'Tanya AI';\n\n  @override\n  String get askAiApiKey => 'Kunci API';\n\n  @override\n  String get askAiAwaitingResponse => 'Menunggu respons AI...';\n\n  @override\n  String get askAiBaseUrl => 'URL dasar';\n\n  @override\n  String get askAiCommandInserted => 'Perintah dimasukkan ke terminal';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Harap konfigurasikan $fields di Pengaturan.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Konfirmasi sebelum menjalankan';\n\n  @override\n  String get askAiConversation => 'Percakapan AI';\n\n  @override\n  String get askAiDisclaimer => 'AI bisa saja salah. Gunakan dengan hati-hati.';\n\n  @override\n  String get askAiFollowUpHint => 'Ajukan pertanyaan lanjutan...';\n\n  @override\n  String get askAiInsertTerminal => 'Masukkan ke terminal';\n\n  @override\n  String get askAiNoResponse => 'Tidak ada respons';\n\n  @override\n  String get askAiRecommendedCommand => 'Perintah yang disarankan AI';\n\n  @override\n  String get askAiSelectedContent => 'Konten yang dipilih';\n\n  @override\n  String get askAiUsageHint => 'Digunakan di Terminal SSH';\n\n  @override\n  String get atLeastOneTab => 'Setidaknya satu tab harus dipilih';\n\n  @override\n  String get authFailTip =>\n      'Otentikasi gagal, silakan periksa apakah kata sandi/kunci/host/pengguna, dll, salah.';\n\n  @override\n  String get autoBackupConflict =>\n      'Hanya satu pencadangan otomatis yang dapat diaktifkan pada saat yang bersamaan.';\n\n  @override\n  String get autoConnect => 'Hubungkan otomatis';\n\n  @override\n  String get autoRun => 'Berjalan Otomatis';\n\n  @override\n  String get autoUpdateHomeWidget => 'Widget Rumah Pembaruan Otomatis';\n\n  @override\n  String get availableTabs => 'Tab Tersedia';\n\n  @override\n  String get backupEncrypted => 'Cadangan telah dienkripsi';\n\n  @override\n  String get backupNotEncrypted => 'Cadangan tidak dienkripsi';\n\n  @override\n  String get backupPassword => 'Kata sandi cadangan';\n\n  @override\n  String get backupPasswordRemoved => 'Kata sandi cadangan dihapus';\n\n  @override\n  String get backupPasswordSet => 'Kata sandi cadangan ditetapkan';\n\n  @override\n  String get backupPasswordTip =>\n      'Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.';\n\n  @override\n  String get backupPasswordWrong => 'Kata sandi cadangan salah';\n\n  @override\n  String get backupTip =>\n      'Data yang diekspor dapat dienkripsi dengan kata sandi. \\nHarap jaga keamanannya.';\n\n  @override\n  String get backupVersionNotMatch => 'Versi cadangan tidak cocok.';\n\n  @override\n  String get bgRun => 'Jalankan di Backgroud';\n\n  @override\n  String get bgRunTip =>\n      'Sakelar ini hanya berarti aplikasi akan mencoba berjalan di latar belakang, apakah aplikasi dapat berjalan di latar belakang tergantung pada apakah izin diaktifkan atau tidak. Untuk Android asli, nonaktifkan \\\"Pengoptimalan Baterai\\\" di aplikasi ini, dan untuk miui, ubah kebijakan penghematan daya ke \\\"Tidak Terbatas\\\".';\n\n  @override\n  String get clearAllStatsContent =>\n      'Apakah Anda yakin ingin menghapus semua statistik koneksi server? Tindakan ini tidak dapat dibatalkan.';\n\n  @override\n  String get clearAllStatsTitle => 'Hapus Semua Statistik';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return 'Apakah Anda yakin ingin menghapus statistik koneksi untuk server \\\"$serverName\\\"? Tindakan ini tidak dapat dibatalkan.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return 'Hapus Statistik $serverName';\n  }\n\n  @override\n  String get clearThisServerStats => 'Hapus Statistik Server Ini';\n\n  @override\n  String get compactDatabase => 'Kompres Database';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Ukuran database: $size\\n\\nIni akan mengatur ulang database untuk mengurangi ukuran file. Tidak ada data yang akan dihapus.';\n  }\n\n  @override\n  String get closeAfterSave => 'Simpan dan tutup';\n\n  @override\n  String get collapseUITip =>\n      'Apakah akan menciutkan daftar panjang yang ada di UI secara default atau tidak';\n\n  @override\n  String get connectionDetails => 'Detail Koneksi';\n\n  @override\n  String get connectionStats => 'Statistik Koneksi';\n\n  @override\n  String get connectionStatsDesc =>\n      'Lihat tingkat keberhasilan koneksi server dan riwayat';\n\n  @override\n  String get containerTrySudoTip =>\n      'Contohnya: Di dalam aplikasi, pengguna diatur sebagai aaa, tetapi Docker diinstal di bawah pengguna root. Dalam kasus ini, Anda perlu mengaktifkan opsi ini.';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Kata sandi sudo diperlukan untuk mengakses Docker. Silakan masukkan kata sandi Anda.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'Kata sandi sudo salah atau tidak diizinkan. Silakan coba lagi.';\n\n  @override\n  String get convert => 'Mengubah';\n\n  @override\n  String get copyPath => 'Path Copy';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Tampilkan tingkat penggunaan setiap CPU dalam gaya bilah kemajuan (gaya lama)';\n\n  @override\n  String get cursorType => 'Jenis kursor';\n\n  @override\n  String get customCmd => 'Perintah kustom';\n\n  @override\n  String get customCmdHint => '\\\"Nama Perintah\\\": \\\"Perintah\\\"';\n\n  @override\n  String get deleteServers => 'Penghapusan server secara batch';\n\n  @override\n  String get desktopTerminalTip =>\n      'Perintah yang digunakan untuk membuka emulator terminal saat memulai sesi SSH.';\n\n  @override\n  String get dirEmpty => 'Pastikan dir kosong.';\n\n  @override\n  String get discoverSshServers => 'Temukan Server SSH';\n\n  @override\n  String get discoveryFailed => 'Penemuan gagal';\n\n  @override\n  String get discoverySettings => 'Pengaturan Penemuan';\n\n  @override\n  String get discoverySummary => 'Ringkasan Penemuan';\n\n  @override\n  String get diskHealth => 'Kesehatan disk';\n\n  @override\n  String get diskIgnorePath => 'Abaikan jalan untuk disk';\n\n  @override\n  String get displayCpuIndex => 'Tampilkan indeks CPU';\n\n  @override\n  String dl2Local(Object fileName) {\n    return 'Unduh $fileName ke lokal?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'Tidak ada wadah yang sedang berjalan.\\nHal ini dapat terjadi karena:\\n- Pengguna instalasi Docker tidak sama dengan nama pengguna yang dikonfigurasi di dalam Aplikasi.\\n- Variabel lingkungan DOCKER_HOST tidak terbaca dengan benar. Anda bisa mendapatkannya dengan menjalankan `echo \\$DOCKER_HOST` di terminal.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '$count gambar';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker tidak terpasang';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount running, $stoppedCount container stopped.';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count wadah berjalan.';\n  }\n\n  @override\n  String get doubleColumnMode => 'Mode kolom ganda';\n\n  @override\n  String get doubleColumnTip =>\n      'Opsi ini hanya mengaktifkan fitur, apakah itu benar-benar dapat diaktifkan tergantung pada lebar perangkat';\n\n  @override\n  String get editVirtKeys => 'Edit kunci virtual';\n\n  @override\n  String get editorHighlightTip =>\n      'Performa penyorotan kode saat ini lebih buruk, dan dapat dimatikan secara opsional untuk perbaikan.';\n\n  @override\n  String get enableMdns => 'Aktifkan mDNS';\n\n  @override\n  String get enableMdnsDesc =>\n      'Gunakan mDNS/Bonjour untuk menemukan layanan SSH';\n\n  @override\n  String get envVars => 'Variabel lingkungan';\n\n  @override\n  String get experimentalFeature => 'Fitur eksperimental';\n\n  @override\n  String get extraArgs => 'Args ekstra';\n\n  @override\n  String get fallbackSshDest => 'Tujuan SSH mundur';\n\n  @override\n  String get fdroidReleaseTip =>\n      'Jika Anda mengunduh aplikasi ini dari F-Droid, disarankan untuk mematikan opsi ini.';\n\n  @override\n  String get fgService => 'Layanan Latar Depan';\n\n  @override\n  String get fgServiceTip =>\n      'Setelah diaktifkan, beberapa model perangkat mungkin crash. Menonaktifkannya dapat menyebabkan beberapa model tidak dapat mempertahankan koneksi SSH di latar belakang. Harap izinkan perizinan notifikasi ServerBox, menjalankan di latar belakang, dan bangun mandiri di pengaturan sistem.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'File \\'$file\\' terlalu besar $size, max $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Selesai pada';\n\n  @override\n  String get followSystem => 'Ikuti sistem';\n\n  @override\n  String get fontSize => 'Ukuran huruf';\n\n  @override\n  String get fullScreen => 'Mode Layar Penuh';\n\n  @override\n  String get fullScreenJitter => 'Jitter layar penuh';\n\n  @override\n  String get fullScreenJitterHelp => 'Untuk menghindari pembakaran layar';\n\n  @override\n  String get fullScreenTip =>\n      'Apakah mode layar penuh diaktifkan ketika perangkat diputar ke modus lanskap? Opsi ini hanya berlaku untuk tab server.';\n\n  @override\n  String get goBackQ => 'Datang kembali?';\n\n  @override\n  String get goto => 'Pergi ke';\n\n  @override\n  String get hideTitleBar => 'Sembunyikan bilah judul';\n\n  @override\n  String get highlight => 'Sorotan kode';\n\n  @override\n  String get homeTabs => 'Tab Beranda';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Sesuaikan tab mana yang muncul di halaman beranda dan urutannya';\n\n  @override\n  String get homeWidgetUrlConfig => 'Konfigurasi URL Widget Rumah';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'Permintaan gagal, kode status: $code';\n  }\n\n  @override\n  String get ignoreCert => 'Abaikan sertifikat';\n\n  @override\n  String get image => 'Gambar';\n\n  @override\n  String get imagesList => 'Daftar gambar';\n\n  @override\n  String get installDockerWithUrl =>\n      'Silakan https://docs.docker.com/engine/install Docker pertama.';\n\n  @override\n  String get invalid => 'Tidak valid';\n\n  @override\n  String get invalidHostFormat =>\n      'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.';\n\n  @override\n  String get jumpServer => 'Lompat server';\n\n  @override\n  String get keepForeground => 'Simpan Aplikasi Foreground!';\n\n  @override\n  String get keepStatusWhenErr => 'Menyimpan status server terakhir';\n\n  @override\n  String get keepStatusWhenErrTip =>\n      'Hanya ketika terjadi kesalahan saat menjalankan skrip';\n\n  @override\n  String get keyAuth => 'Auth kunci';\n\n  @override\n  String get lastFailure => 'Gagal Terakhir';\n\n  @override\n  String get lastSuccess => 'Sukses Terakhir';\n\n  @override\n  String get letterCache => 'Caching huruf';\n\n  @override\n  String get letterCacheTip =>\n      'Direkomendasikan untuk menonaktifkan, tetapi setelah dinonaktifkan, tidak mungkin untuk memasukkan karakter CJK.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return 'Dibuat dengan ❤️ oleh $myGithub';\n  }\n\n  @override\n  String get max => 'Max';\n\n  @override\n  String get maxConcurrency => 'Konkurensi Maksimum';\n\n  @override\n  String get maxRetryCount => 'Jumlah penyambungan kembali server';\n\n  @override\n  String get maxRetryCountEqual0 => 'Akan mencoba lagi lagi dan lagi.';\n\n  @override\n  String get min => 'Min';\n\n  @override\n  String get more => 'Lebih Banyak';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'Aktif: dapat ditampilkan di bawah setiap kartu pada halaman Tab Server. Nonaktif: dapat ditampilkan di bagian atas halaman Rincian Server.';\n\n  @override\n  String get needHomeDir =>\n      'Jika Anda pengguna Synology, [lihat di sini](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Pengguna sistem lain perlu mencari cara membuat direktori home.';\n\n  @override\n  String get needRestart => 'Perlu memulai ulang aplikasi';\n\n  @override\n  String get netViewType => 'Jenis tampilan bersih';\n\n  @override\n  String get newContainer => 'Wadah baru';\n\n  @override\n  String get noConnectionStatsData => 'Tidak ada data statistik koneksi';\n\n  @override\n  String get noLineChart => 'Jangan gunakan grafik garis';\n\n  @override\n  String get noLineChartForCpu => 'Jangan gunakan diagram garis untuk CPU';\n\n  @override\n  String get noPrivateKeyTip =>\n      'Kunci privat tidak ada, mungkin telah dihapus atau ada kesalahan konfigurasi.';\n\n  @override\n  String get noPromptAgain => 'Jangan tanya lagi';\n\n  @override\n  String get onServerDetailPage => 'Di halaman detail server';\n\n  @override\n  String get onlyOneLine =>\n      'Hanya tampilkan sebagai satu baris (dapat digulir)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 => 'Berlaku hanya ketika jumlah inti > 8';\n\n  @override\n  String get openLastPath => 'Buka jalur terakhir';\n\n  @override\n  String get openLastPathTip =>\n      'Server yang berbeda akan memiliki catatan yang berbeda, dan catatan tersebut adalah jalur menuju pintu keluar';\n\n  @override\n  String get parseContainerStatsTip =>\n      'Parsing status okupansi oleh Docker agak lambat';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$percent% dari $size';\n  }\n\n  @override\n  String get permission => 'Izin';\n\n  @override\n  String get pingInputIP => 'Harap masukkan IP / domain target.';\n\n  @override\n  String get pingNoServer =>\n      'Tidak ada server untuk melakukan ping.\\nHarap tambahkan server di tab Server.';\n\n  @override\n  String get plugInType => 'Jenis Penyisipan';\n\n  @override\n  String get preferDiskAmount => 'Prioritaskan tampilan kapasitas disk';\n\n  @override\n  String get privateKey => 'Kunci Pribadi';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'Kunci privat [$keyId] tidak ditemukan.';\n  }\n\n  @override\n  String get pushToken => 'Dorong token';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'Tidak disarankan untuk diaktifkan, waspadai risiko keamanan! Jika Anda menggunakan sertifikat default dari PVE, Anda perlu mengaktifkan opsi ini.';\n\n  @override\n  String get pveLoginFailed =>\n      'Login gagal. Tidak dapat mengautentikasi dengan nama pengguna/kata sandi dari konfigurasi server untuk login Linux PAM.';\n\n  @override\n  String get pveVersionLow =>\n      'Fitur ini saat ini sedang dalam tahap pengujian dan hanya diuji pada PVE 8+. Gunakan dengan hati-hati.';\n\n  @override\n  String get read => 'Baca';\n\n  @override\n  String get recentConnections => 'Koneksi Terkini';\n\n  @override\n  String get rememberPwdInMem => 'Ingat kata sandi di dalam memori';\n\n  @override\n  String get rememberPwdInMemTip =>\n      'Digunakan untuk kontainer, menangguhkan, dll.';\n\n  @override\n  String get rememberWindowSize => 'Ingat ukuran jendela';\n\n  @override\n  String get remotePath => 'Jalur jarak jauh';\n\n  @override\n  String get result => 'Hasil';\n\n  @override\n  String get rotateAngel => 'Sudut rotasi';\n\n  @override\n  String get sameIdServerExist => 'Server dengan ID yang sama sudah ada';\n\n  @override\n  String get save => 'Menyimpan';\n\n  @override\n  String get second => 'S';\n\n  @override\n  String get serverDetailOrder => 'Detail pesanan widget halaman';\n\n  @override\n  String get serverFuncBtns => 'Tombol fungsi server';\n\n  @override\n  String get serverOrder => 'Pesanan server';\n\n  @override\n  String get serverTabRequired => 'Tab server tidak dapat dihapus';\n\n  @override\n  String get sftpDlPrepare => 'Bersiap untuk terhubung ...';\n\n  @override\n  String get sftpEditorTip =>\n      'Jika kosong, gunakan editor file bawaan aplikasi. Jika ada nilai, gunakan editor server jarak jauh, misalnya `vim` (disarankan untuk mendeteksi secara otomatis sesuai `EDITOR`).';\n\n  @override\n  String get sftpRmrDirSummary => 'Gunakan `rm -r` untuk menghapus dir di SFTP';\n\n  @override\n  String get sftpSSHConnected => 'Sftp terhubung';\n\n  @override\n  String get sftpShowFoldersFirst => 'Folder ditampilkan lebih dulu';\n\n  @override\n  String get showDistLogo => 'Tampilkan logo distribusi';\n\n  @override\n  String get size => 'Ukuran';\n\n  @override\n  String get softWrap => 'Pembungkus lembut';\n\n  @override\n  String get specifyDev => 'Tentukan perangkat';\n\n  @override\n  String get specifyDevTip =>\n      'Misalnya, statistik lalu lintas jaringan secara default adalah untuk semua perangkat. Anda dapat menentukan perangkat tertentu di sini.';\n\n  @override\n  String get speed => 'Kecepatan';\n\n  @override\n  String spentTime(Object time) {\n    return 'Menghabiskan waktu: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'Semua server sudah ada (ditemukan $duplicateCount duplikat)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount duplikat akan dilewati';\n  }\n\n  @override\n  String get sshConfigFound => 'Kami menemukan konfigurasi SSH di sistem Anda';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return 'Ditemukan $totalCount server';\n  }\n\n  @override\n  String get sshConfigImport => 'Impor Konfigurasi SSH';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Hanya informasi dasar yang dapat diimpor, misalnya: IP/Port.';\n\n  @override\n  String get sshConfigImportPermission =>\n      'Apakah Anda ingin memberikan izin untuk membaca ~/.ssh/config dan secara otomatis mengimpor pengaturan server?';\n\n  @override\n  String get sshConfigImportTip =>\n      'Prompt untuk membaca ~/.ssh/config saat pembuatan server pertama';\n\n  @override\n  String sshConfigImported(Object count) {\n    return 'Berhasil mengimpor $count server dari konfigurasi SSH';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return 'Kunci host SSH untuk $serverName telah berubah. Lanjutkan hanya jika Anda mempercayai server ini.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Sidik jari (MD5 Base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Sidik jari (MD5 hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'Jenis kunci host SSH';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return 'Kunci host SSH baru diterima dari $serverName. Periksa sidik jarinya sebelum mempercayai.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Sidik jari tersimpan: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      'Apakah Anda ingin memilih file konfigurasi SSH secara manual?';\n\n  @override\n  String get sshConfigNoServers =>\n      'Tidak ada server yang ditemukan dalam konfigurasi SSH';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'Tidak dapat mengakses file konfigurasi SSH karena izin macOS.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount server akan diimpor';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'Ketika terminal dapat digulirkan, menggeser secara horizontal dapat memilih teks. Mengklik tombol keyboard mengaktifkan/menonaktifkan keyboard. Ikon file membuka SFTP jalur saat ini. Tombol papan klip menyalin konten saat teks dipilih, dan menempelkan konten dari papan klip ke terminal saat tidak ada teks yang dipilih dan ada konten di papan klip. Ikon kode menempelkan potongan kode ke terminal dan mengeksekusinya.';\n\n  @override\n  String sshTip(Object url) {\n    return 'Fungsi ini sekarang dalam tahap eksperimen.\\n\\nHarap laporkan bug di $url atau bergabunglah dengan pengembangan kami.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff => 'Switching Otomatis Kunci Virtual';\n\n  @override\n  String get stat => 'Statistik';\n\n  @override\n  String get supportFmtArgs => 'Parameter pemformatan berikut ini didukung:';\n\n  @override\n  String get suspendTip =>\n      'Fungsi penangguhan memerlukan hak akses root dan dukungan systemd.';\n\n  @override\n  String switchTo(Object val) {\n    return 'Beralih ke $val';\n  }\n\n  @override\n  String get syncTip =>\n      'Pengaktifan ulang mungkin diperlukan agar beberapa perubahan dapat diterapkan.';\n\n  @override\n  String get system => 'Sistem';\n\n  @override\n  String get tag => 'Tag';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Tekan tombol pencarian untuk menemukan server SSH di jaringan Anda';\n\n  @override\n  String get termFontSizeTip =>\n      'Pengaturan ini akan memengaruhi ukuran terminal (lebar dan tinggi). Anda dapat melakukan zoom pada halaman terminal untuk menyesuaikan ukuran font sesi saat ini.';\n\n  @override\n  String get textScaler => 'Penskalaan font';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (ukuran asli), hanya berfungsi pada bagian halaman server font, tidak disarankan untuk diubah.';\n\n  @override\n  String get time => 'Waktu';\n\n  @override\n  String get times => 'Waktu';\n\n  @override\n  String get trySudo => 'Cobalah menggunakan sudo';\n\n  @override\n  String get unknown => 'Tidak dikenal';\n\n  @override\n  String get unkownConvertMode => 'Mode Konversi Tidak Diketahui';\n\n  @override\n  String get update => 'Memperbarui';\n\n  @override\n  String get updateIntervalEqual0 =>\n      'Anda mengatur ke 0, tidak akan memperbarui secara otomatis.\\nTidak dapat menghitung status CPU.';\n\n  @override\n  String get updateServerStatusInterval => 'Interval Pembaruan Status Server';\n\n  @override\n  String get upsideDown => 'Terbalik';\n\n  @override\n  String get useCdn => 'Menggunakan CDN';\n\n  @override\n  String get useCdnTip =>\n      'Pengguna non-Cina disarankan menggunakan CDN. Apakah Anda ingin menggunakannya?';\n\n  @override\n  String get useNoPwd => 'Tidak ada kata sandi yang akan digunakan';\n\n  @override\n  String get usePodmanByDefault => 'Menggunakan Podman sebagai bawaan';\n\n  @override\n  String get used => 'Digunakan';\n\n  @override\n  String get view => 'Tampilan';\n\n  @override\n  String get viewDetails => 'Lihat Detail';\n\n  @override\n  String get viewErr => 'Lihat kesalahan';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'Salin ke clipboard jika terminal yang dipilih tidak kosong, jika tidak, tempel isi clipboard ke terminal.';\n\n  @override\n  String get virtKeyHelpIME => 'Menyalakan/mematikan keyboard';\n\n  @override\n  String get virtKeyHelpSFTP => 'Buka direktori saat ini di SFTP.';\n\n  @override\n  String get waitConnection => 'Harap tunggu koneksi akan dibuat.';\n\n  @override\n  String get wakeLock => 'Tetap terjaga';\n\n  @override\n  String get watchNotPaired => 'Tidak ada Apple Watch yang dipasangkan';\n\n  @override\n  String get webdavSettingEmpty => 'Pengaturan webdav kosong';\n\n  @override\n  String get whenOpenApp => 'Saat membuka aplikasi';\n\n  @override\n  String get wolTip =>\n      'Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.';\n\n  @override\n  String get write => 'Tulis';\n\n  @override\n  String get writeScriptFailTip =>\n      'Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.';\n\n  @override\n  String get writeScriptTip =>\n      'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \\n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Emulasi Podman Docker terdeteksi. Silakan beralih ke Podman di pengaturan.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_it.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Italian (`it`).\nclass AppLocalizationsIt extends AppLocalizations {\n  AppLocalizationsIt([String locale = 'it']) : super(locale);\n\n  @override\n  String get aboutThanks =>\n      'Grazie alle seguenti persone che hanno partecipato.';\n\n  @override\n  String get acceptBeta => 'Accetta aggiornamenti versione beta';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'Attualmente non esistono chiavi private, vuoi aggiungere quella fornita dal sistema (~/.ssh/id_rsa)?';\n\n  @override\n  String get added2List => 'Aggiunto alla lista delle attività';\n\n  @override\n  String get addr => 'Indirizzo';\n\n  @override\n  String get alreadyLastDir => 'Già nell\\'ultima directory.';\n\n  @override\n  String get askAi => 'Chiedi all\\'IA';\n\n  @override\n  String get askAiApiKey => 'Chiave API';\n\n  @override\n  String get askAiAwaitingResponse => 'In attesa della risposta dell\\'IA...';\n\n  @override\n  String get askAiBaseUrl => 'URL base';\n\n  @override\n  String get askAiCommandInserted => 'Comando inserito nel terminale';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Configura $fields in Impostazioni.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Conferma prima di eseguire';\n\n  @override\n  String get askAiConversation => 'Conversazione IA';\n\n  @override\n  String get askAiDisclaimer =>\n      'L\\'IA potrebbe essere errata. Rivedi attentamente prima di applicare.';\n\n  @override\n  String get askAiFollowUpHint => 'Fai una domanda di follow-up...';\n\n  @override\n  String get askAiInsertTerminal => 'Inserisci nel terminale';\n\n  @override\n  String get askAiNoResponse => 'Nessuna risposta';\n\n  @override\n  String get askAiRecommendedCommand => 'Comando suggerito dall\\'IA';\n\n  @override\n  String get askAiSelectedContent => 'Contenuto selezionato';\n\n  @override\n  String get askAiUsageHint => 'Utilizzato nel Terminale SSH';\n\n  @override\n  String get atLeastOneTab => 'Deve essere selezionata almeno una scheda';\n\n  @override\n  String get authFailTip =>\n      'Autenticazione fallita, verifica se le credenziali sono corrette';\n\n  @override\n  String get autoBackupConflict =>\n      'Solo un backup automatico può essere attivato alla volta.';\n\n  @override\n  String get autoConnect => 'Connessione automatica';\n\n  @override\n  String get autoRun => 'Esecuzione automatica';\n\n  @override\n  String get autoUpdateHomeWidget => 'Aggiornamento automatico widget home';\n\n  @override\n  String get availableTabs => 'Schede disponibili';\n\n  @override\n  String get backupEncrypted => 'Il backup è crittografato';\n\n  @override\n  String get backupNotEncrypted => 'Il backup non è crittografato';\n\n  @override\n  String get backupPassword => 'Password di backup';\n\n  @override\n  String get backupPasswordRemoved => 'Password di backup rimossa';\n\n  @override\n  String get backupPasswordSet => 'Password di backup impostata';\n\n  @override\n  String get backupPasswordTip =>\n      'Imposta una password per crittografare i file di backup. Lascia vuoto per disabilitare la crittografia.';\n\n  @override\n  String get backupPasswordWrong => 'Password di backup errata';\n\n  @override\n  String get backupTip =>\n      'I dati esportati possono essere crittografati con password.\\nConservali al sicuro.';\n\n  @override\n  String get backupVersionNotMatch => 'La versione del backup non corrisponde.';\n\n  @override\n  String get bgRun => 'Esegui in background';\n\n  @override\n  String get bgRunTip =>\n      'Questa opzione significa solo che il programma cercherà di eseguire in background. Se può eseguire in background dipende dal fatto che il permesso sia abilitato o meno. Per le ROM Android basate su AOSP, disabilita \\\"Ottimizzazione batteria\\\" in questa app. Per MIUI/HyperOS, cambia la politica di risparmio energetico su \\\"Illimitato\\\".';\n\n  @override\n  String get clearAllStatsContent =>\n      'Sei sicuro di voler cancellare tutte le statistiche di connessione del server? Questa azione non può essere annullata.';\n\n  @override\n  String get clearAllStatsTitle => 'Cancella tutte le statistiche';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return 'Sei sicuro di voler cancellare le statistiche di connessione per il server \\\"$serverName\\\"? Questa azione non può essere annullata.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return 'Cancella statistiche $serverName';\n  }\n\n  @override\n  String get clearThisServerStats => 'Cancella statistiche di questo server';\n\n  @override\n  String get compactDatabase => 'Compatta database';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Dimensione database: $size\\n\\nQuesto riorganizzerà il database per ridurre la dimensione del file. Nessun dato verrà eliminato.';\n  }\n\n  @override\n  String get closeAfterSave => 'Salva e chiudi';\n\n  @override\n  String get collapseUITip =>\n      'Se comprimere le liste lunghe presenti nell\\'interfaccia utente per impostazione predefinita';\n\n  @override\n  String get connectionDetails => 'Dettagli connessione';\n\n  @override\n  String get connectionStats => 'Statistiche connessione';\n\n  @override\n  String get connectionStatsDesc =>\n      'Visualizza il tasso di successo della connessione al server e la cronologia';\n\n  @override\n  String get containerTrySudoTip =>\n      'Ad esempio: nell\\'app, l\\'utente è impostato su aaa, ma Docker è installato sotto l\\'utente root. In questo caso, devi abilitare questa opzione.';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'È richiesta la password sudo per accedere a Docker. Inserisci la tua password.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'La password sudo è errata o non consentita. Riprova.';\n\n  @override\n  String get convert => 'Converti';\n\n  @override\n  String get copyPath => 'Copia percorso';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Visualizza l\\'utilizzo di ogni CPU in stile barra di avanzamento (stile vecchio)';\n\n  @override\n  String get cursorType => 'Tipo di cursore';\n\n  @override\n  String get customCmd => 'Comandi personalizzati';\n\n  @override\n  String get customCmdHint => '\\\"Nome comando\\\": \\\"Comando\\\"';\n\n  @override\n  String get deleteServers => 'Elimina server in blocco';\n\n  @override\n  String get desktopTerminalTip =>\n      'Comando utilizzato per aprire l\\'emulatore di terminale quando si avviano sessioni SSH.';\n\n  @override\n  String get dirEmpty => 'Assicurati che la cartella sia vuota.';\n\n  @override\n  String get discoverSshServers => 'Scopri server SSH';\n\n  @override\n  String get discoveryFailed => 'Scoperta fallita';\n\n  @override\n  String get discoverySettings => 'Impostazioni scoperta';\n\n  @override\n  String get discoverySummary => 'Riepilogo scoperta';\n\n  @override\n  String get diskHealth => 'Salute disco';\n\n  @override\n  String get diskIgnorePath => 'Ignora percorso per disco';\n\n  @override\n  String get displayCpuIndex => 'Mostra indice CPU';\n\n  @override\n  String dl2Local(Object fileName) {\n    return 'Scaricare $fileName in locale?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'Non ci sono container in esecuzione.\\nQuesto potrebbe essere perché:\\n- L\\'utente di installazione di Docker non è lo stesso del nome utente configurato nell\\'App.\\n- La variabile d\\'ambiente DOCKER_HOST non è stata letta correttamente. Puoi ottenerla eseguendo `echo \\$DOCKER_HOST` nel terminale.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '$count immagini';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker non installato';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount in esecuzione, $stoppedCount container fermati.';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count container in esecuzione.';\n  }\n\n  @override\n  String get doubleColumnMode => 'Modalità a doppia colonna';\n\n  @override\n  String get doubleColumnTip =>\n      'Questa opzione abilita solo la funzione, se può essere effettivamente abilitata dipende dalla larghezza del dispositivo';\n\n  @override\n  String get editVirtKeys => 'Modifica tasti virtuali';\n\n  @override\n  String get editorHighlightTip =>\n      'Le attuali prestazioni di evidenziazione del codice non sono ideali e possono essere disabilitate opzionalmente per migliorare.';\n\n  @override\n  String get enableMdns => 'Abilita mDNS';\n\n  @override\n  String get enableMdnsDesc => 'Usa mDNS/Bonjour per scoprire servizi SSH';\n\n  @override\n  String get envVars => 'Variabile d\\'ambiente';\n\n  @override\n  String get experimentalFeature => 'Funzionalità sperimentale';\n\n  @override\n  String get extraArgs => 'Argomenti extra';\n\n  @override\n  String get fallbackSshDest => 'Destinazione SSH di fallback';\n\n  @override\n  String get fdroidReleaseTip =>\n      'Se hai scaricato questa app da F-Droid, si consiglia di disattivare questa opzione.';\n\n  @override\n  String get fgService => 'Servizio in primo piano';\n\n  @override\n  String get fgServiceTip =>\n      'Dopo l\\'attivazione, alcuni modelli di dispositivo potrebbero arrestarsi in modo anomalo. Disabilitarlo potrebbe causare l\\'impossibilità per alcuni modelli di mantenere le connessioni SSH in background. Consenti le autorizzazioni di notifica ServerBox, l\\'esecuzione in background e l\\'auto-riattivazione nelle impostazioni di sistema.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'File \\'$file\\' troppo grande $size, max $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Completato alle';\n\n  @override\n  String get followSystem => 'Segui sistema';\n\n  @override\n  String get fontSize => 'Dimensione carattere';\n\n  @override\n  String get fullScreen => 'Modalità schermo intero';\n\n  @override\n  String get fullScreenJitter => 'Jitter schermo intero';\n\n  @override\n  String get fullScreenJitterHelp => 'Per evitare il burn-in dello schermo';\n\n  @override\n  String get fullScreenTip =>\n      'La modalità a schermo intero deve essere abilitata quando il dispositivo viene ruotato in modalità orizzontale? Questa opzione si applica solo alla scheda server.';\n\n  @override\n  String get goBackQ => 'Tornare indietro?';\n\n  @override\n  String get goto => 'Vai a';\n\n  @override\n  String get hideTitleBar => 'Nascondi barra del titolo';\n\n  @override\n  String get highlight => 'Evidenziazione codice';\n\n  @override\n  String get homeTabs => 'Schede home';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Personalizza quali schede appaiono nella home page e il loro ordine';\n\n  @override\n  String get homeWidgetUrlConfig => 'Configura url widget home';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'richiesta fallita, codice stato: $code';\n  }\n\n  @override\n  String get ignoreCert => 'Ignora certificato';\n\n  @override\n  String get image => 'Immagine';\n\n  @override\n  String get imagesList => 'Elenco immagini';\n\n  @override\n  String get installDockerWithUrl =>\n      'Installa prima docker da https://docs.docker.com/engine/install .';\n\n  @override\n  String get invalid => 'Non valido';\n\n  @override\n  String get invalidHostFormat =>\n      'Formato host non valido. Sono consentiti solo caratteri IPv4, IPv6 e di dominio.';\n\n  @override\n  String get jumpServer => 'Server di salto';\n\n  @override\n  String get keepForeground => 'Mantieni l\\'app in primo piano!';\n\n  @override\n  String get keepStatusWhenErr => 'Conserva l\\'ultimo stato del server';\n\n  @override\n  String get keepStatusWhenErrTip =>\n      'Solo in caso di errore durante l\\'esecuzione dello script';\n\n  @override\n  String get keyAuth => 'Autenticazione chiave';\n\n  @override\n  String get lastFailure => 'Ultimo fallimento';\n\n  @override\n  String get lastSuccess => 'Ultimo successo';\n\n  @override\n  String get letterCache => 'Cache lettere';\n\n  @override\n  String get letterCacheTip =>\n      'Si consiglia di disabilitare, ma dopo aver disabilitato, non sarà possibile inserire caratteri CJK.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return 'Realizzato con ❤️ da $myGithub';\n  }\n\n  @override\n  String get max => 'max';\n\n  @override\n  String get maxConcurrency => 'Massima concorrenza';\n\n  @override\n  String get maxRetryCount => 'Numero di riconnessioni del server';\n\n  @override\n  String get maxRetryCountEqual0 => 'Proverà di nuovo e ancora.';\n\n  @override\n  String get min => 'min';\n\n  @override\n  String get more => 'Altro';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'Attivo: può essere visualizzato sotto ogni carta nella pagina Scheda Server. Disattivato: può essere visualizzato nella parte superiore della pagina Dettagli Server.';\n\n  @override\n  String get needHomeDir =>\n      'Se sei un utente Synology, [vedi qui](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Gli utenti di altri sistemi devono cercare come creare una directory home.';\n\n  @override\n  String get needRestart => 'L\\'app deve essere riavviata';\n\n  @override\n  String get netViewType => 'Tipo di visualizzazione rete';\n\n  @override\n  String get newContainer => 'Nuovo container';\n\n  @override\n  String get noConnectionStatsData =>\n      'Nessun dato di statistiche di connessione';\n\n  @override\n  String get noLineChart => 'Non usare grafici a linee';\n\n  @override\n  String get noLineChartForCpu => 'Non usare grafici a linee per la CPU';\n\n  @override\n  String get noPrivateKeyTip =>\n      'La chiave privata non esiste, potrebbe essere stata eliminata o c\\'è un errore di configurazione.';\n\n  @override\n  String get noPromptAgain => 'Non chiedere di nuovo';\n\n  @override\n  String get onServerDetailPage => 'Nella pagina dettagli server';\n\n  @override\n  String get onlyOneLine => 'Visualizza solo come una riga (scorrevole)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 =>\n      'Funziona solo quando il numero di core è maggiore di 8';\n\n  @override\n  String get openLastPath => 'Apri l\\'ultimo percorso';\n\n  @override\n  String get openLastPathTip =>\n      'Server diversi avranno log diversi e il log è il percorso di uscita';\n\n  @override\n  String get parseContainerStatsTip =>\n      'L\\'analisi dello stato di occupazione di Docker è relativamente lenta.';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$percent% di $size';\n  }\n\n  @override\n  String get permission => 'Permessi';\n\n  @override\n  String get pingInputIP => 'Inserisci un IP / dominio di destinazione.';\n\n  @override\n  String get pingNoServer =>\n      'Nessun server da pingare.\\nAggiungi un server nella scheda server.';\n\n  @override\n  String get plugInType => 'Tipo di inserimento';\n\n  @override\n  String get preferDiskAmount => 'Priorità visualizzazione capacità disco';\n\n  @override\n  String get privateKey => 'Chiave privata';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'Chiave privata [$keyId] non trovata.';\n  }\n\n  @override\n  String get pushToken => 'Token push';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'Non si consiglia di abilitare, attento ai rischi per la sicurezza! Se stai usando il certificato predefinito da PVE, devi abilitare questa opzione.';\n\n  @override\n  String get pveLoginFailed =>\n      'Accesso fallito. Impossibile autenticarsi con nome utente/password dalla configurazione del server per l\\'accesso Linux PAM.';\n\n  @override\n  String get pveVersionLow =>\n      'Questa funzionalità è attualmente nella fase di test ed è stata testata solo su PVE 8+. Usala con cautela.';\n\n  @override\n  String get read => 'Leggi';\n\n  @override\n  String get recentConnections => 'Connessioni recenti';\n\n  @override\n  String get rememberPwdInMem => 'Ricorda password in memoria';\n\n  @override\n  String get rememberPwdInMemTip =>\n      'Utilizzato per container, sospensione, ecc.';\n\n  @override\n  String get rememberWindowSize => 'Ricorda dimensione finestra';\n\n  @override\n  String get remotePath => 'Percorso remoto';\n\n  @override\n  String get result => 'Risultato';\n\n  @override\n  String get rotateAngel => 'Angolo di rotazione';\n\n  @override\n  String get sameIdServerExist => 'Esiste già un server con lo stesso ID';\n\n  @override\n  String get save => 'Salva';\n\n  @override\n  String get second => 's';\n\n  @override\n  String get serverDetailOrder => 'Ordine widget pagina dettagli';\n\n  @override\n  String get serverFuncBtns => 'Pulsanti funzione server';\n\n  @override\n  String get serverOrder => 'Ordine server';\n\n  @override\n  String get serverTabRequired => 'La scheda server non può essere rimossa';\n\n  @override\n  String get sftpDlPrepare => 'Preparazione alla connessione...';\n\n  @override\n  String get sftpEditorTip =>\n      'Se vuoto, usa l\\'editor di file integrato dell\\'app. Se è presente un valore, usa l\\'editor del server remoto, ad es. `vim` (si consiglia di rilevare automaticamente secondo `EDITOR`).';\n\n  @override\n  String get sftpRmrDirSummary =>\n      'Usa `rm -r` per eliminare una cartella in SFTP.';\n\n  @override\n  String get sftpSSHConnected => 'SFTP connesso';\n\n  @override\n  String get sftpShowFoldersFirst => 'Mostra prima le cartelle';\n\n  @override\n  String get showDistLogo => 'Mostra logo distribuzione';\n\n  @override\n  String get size => 'Dimensione';\n\n  @override\n  String get softWrap => 'A capo automatico';\n\n  @override\n  String get specifyDev => 'Specifica dispositivo';\n\n  @override\n  String get specifyDevTip =>\n      'Ad esempio, le statistiche del traffico di rete sono per impostazione predefinita per tutti i dispositivi. Puoi specificare un dispositivo particolare qui.';\n\n  @override\n  String get speed => 'Velocità';\n\n  @override\n  String spentTime(Object time) {\n    return 'Tempo impiegato: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'Tutti i server esistono già ($duplicateCount duplicati trovati)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount duplicati verranno saltati';\n  }\n\n  @override\n  String get sshConfigFound =>\n      'Abbiamo trovato la configurazione SSH sul tuo sistema.';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return 'Trovati $totalCount server';\n  }\n\n  @override\n  String get sshConfigImport => 'Importa configurazione SSH';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Solo le informazioni di base possono essere importate, ad esempio: IP/Porta.';\n\n  @override\n  String get sshConfigImportPermission =>\n      'Vuoi dare il permesso di leggere ~/.ssh/config e importare automaticamente le impostazioni del server?';\n\n  @override\n  String get sshConfigImportTip =>\n      'Chiedi di leggere ~/.ssh/config alla prima creazione del server';\n\n  @override\n  String sshConfigImported(Object count) {\n    return 'Importati $count server dalla configurazione SSH';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return 'La chiave host SSH è cambiata per $serverName. Continua solo se ti fidi di questo server.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Impronta digitale (MD5 base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Impronta digitale (MD5 hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'Tipo chiave host SSH';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return 'È stata ricevuta una nuova chiave host SSH da $serverName. Rivedi l\\'impronta digitale prima di fidarti.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Impronta digitale memorizzata: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      'Vuoi selezionare manualmente il file di configurazione SSH?';\n\n  @override\n  String get sshConfigNoServers =>\n      'Nessun server trovato nella configurazione SSH';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'Impossibile accedere al file di configurazione SSH a causa dei permessi macOS.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount server verranno importati';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'Quando il terminale è scorrevole, trascinare orizzontalmente può selezionare il testo. Cliccando il pulsante tastiera accende/spegne la tastiera. L\\'icona file apre il percorso corrente SFTP. Il pulsante appunti copia il contenuto quando il testo è selezionato e incolla il contenuto dagli appunti nel terminale quando nessun testo è selezionato e c\\'è contenuto negli appunti. L\\'icona codice incolla snippet di codice nel terminale ed esegue.';\n\n  @override\n  String sshTip(Object url) {\n    return 'Questa funzione è ora nella fase sperimentale.\\n\\nSegnala i bug su $url o unisciti al nostro sviluppo.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff =>\n      'Commutazione automatica dei tasti virtuali';\n\n  @override\n  String get stat => 'Statistiche';\n\n  @override\n  String get supportFmtArgs =>\n      'Sono supportati i seguenti parametri di formattazione:';\n\n  @override\n  String get suspendTip =>\n      'La funzione di sospensione richiede il permesso root e il supporto systemd.';\n\n  @override\n  String switchTo(Object val) {\n    return 'Passa a $val';\n  }\n\n  @override\n  String get syncTip =>\n      'Potrebbe essere necessario un riavvio affinché alcune modifiche abbiano effetto.';\n\n  @override\n  String get system => 'Sistema';\n\n  @override\n  String get tag => 'Tag';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Tocca il pulsante di ricerca per scoprire i server SSH sulla tua rete';\n\n  @override\n  String get termFontSizeTip =>\n      'Questa impostazione influirà sulla dimensione del terminale (larghezza e altezza). Puoi ingrandire la pagina del terminale per regolare la dimensione del carattere della sessione corrente.';\n\n  @override\n  String get textScaler => 'Scalatore testo';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (dimensione originale), funziona solo su parte del carattere della pagina server, non si consiglia di cambiare.';\n\n  @override\n  String get time => 'Tempo';\n\n  @override\n  String get times => 'Volte';\n\n  @override\n  String get trySudo => 'Prova a usare sudo';\n\n  @override\n  String get unknown => 'Sconosciuto';\n\n  @override\n  String get unkownConvertMode => 'Modalità di conversione sconosciuta';\n\n  @override\n  String get update => 'Aggiorna';\n\n  @override\n  String get updateIntervalEqual0 =>\n      'Hai impostato a 0, non aggiornerà automaticamente.\\nNon può calcolare lo stato della CPU.';\n\n  @override\n  String get updateServerStatusInterval =>\n      'Intervallo di aggiornamento stato server';\n\n  @override\n  String get upsideDown => 'Capovolto';\n\n  @override\n  String get useCdn => 'Utilizzo CDN';\n\n  @override\n  String get useCdnTip =>\n      'Si consiglia agli utenti non cinesi di usare CDN. Vuoi usarlo?';\n\n  @override\n  String get useNoPwd => 'Non verrà usata nessuna password';\n\n  @override\n  String get usePodmanByDefault => 'Usa Podman per impostazione predefinita';\n\n  @override\n  String get used => 'Usato';\n\n  @override\n  String get view => 'Visualizza';\n\n  @override\n  String get viewDetails => 'Visualizza dettagli';\n\n  @override\n  String get viewErr => 'Vedi errore';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'Copia negli appunti se il terminale selezionato non è vuoto, altrimenti incolla il contenuto degli appunti nel terminale.';\n\n  @override\n  String get virtKeyHelpIME => 'Accendi/spegni la tastiera';\n\n  @override\n  String get virtKeyHelpSFTP => 'Apri la directory corrente in SFTP.';\n\n  @override\n  String get waitConnection => 'Attendi che la connessione venga stabilita.';\n\n  @override\n  String get wakeLock => 'Mantieni sveglio';\n\n  @override\n  String get watchNotPaired => 'Nessun Apple Watch associato';\n\n  @override\n  String get webdavSettingEmpty => 'Impostazione WebDav vuota';\n\n  @override\n  String get whenOpenApp => 'All\\'apertura dell\\'app';\n\n  @override\n  String get wolTip =>\n      'Dopo aver configurato WOL (Wake-on-LAN), viene inviata una richiesta WOL ogni volta che il server è connesso.';\n\n  @override\n  String get write => 'Scrivi';\n\n  @override\n  String get writeScriptFailTip =>\n      'Scrittura dello script fallita, forse a causa di mancanza di permessi o la directory non esiste.';\n\n  @override\n  String get writeScriptTip =>\n      'Dopo essersi connessi al server, uno script verrà scritto in `~/.config/server_box` \\n | `/tmp/server_box` per monitorare lo stato del sistema. Puoi rivedere il contenuto dello script.';\n\n  @override\n  String get menuGitHubRepository => 'Repository GitHub';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Rilevata emulazione Docker Podman. Passa a Podman nelle impostazioni.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_ja.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Japanese (`ja`).\nclass AppLocalizationsJa extends AppLocalizations {\n  AppLocalizationsJa([String locale = 'ja']) : super(locale);\n\n  @override\n  String get aboutThanks => '以下の参加者に感謝します。';\n\n  @override\n  String get acceptBeta => 'テストバージョンの更新を受け入れる';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      '現在秘密鍵がありません。システムのデフォルト(~/.ssh/id_rsa)を追加しますか？';\n\n  @override\n  String get added2List => 'タスクリストに追加されました';\n\n  @override\n  String get addr => 'アドレス';\n\n  @override\n  String get alreadyLastDir => 'すでに最上位のディレクトリです';\n\n  @override\n  String get askAi => 'AI に質問';\n\n  @override\n  String get askAiApiKey => 'API キー';\n\n  @override\n  String get askAiAwaitingResponse => 'AI の応答を待機中...';\n\n  @override\n  String get askAiBaseUrl => 'ベース URL';\n\n  @override\n  String get askAiCommandInserted => 'コマンドをターミナルに挿入しました';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return '設定で $fields を構成してください。';\n  }\n\n  @override\n  String get askAiConfirmExecute => '実行前に確認';\n\n  @override\n  String get askAiConversation => 'AI 会話';\n\n  @override\n  String get askAiDisclaimer => 'AI が誤る可能性があります。注意してご利用ください。';\n\n  @override\n  String get askAiFollowUpHint => '追質問をする...';\n\n  @override\n  String get askAiInsertTerminal => 'ターミナルに挿入';\n\n  @override\n  String get askAiNoResponse => '応答なし';\n\n  @override\n  String get askAiRecommendedCommand => 'AI 推奨コマンド';\n\n  @override\n  String get askAiSelectedContent => '選択した内容';\n\n  @override\n  String get askAiUsageHint => 'SSH ターミナルで使用';\n\n  @override\n  String get atLeastOneTab => '少なくとも1つのタブを選択する必要があります';\n\n  @override\n  String get authFailTip => '認証に失敗しました。パスワード/鍵/ホスト/ユーザーなどが間違っていないか確認してください。';\n\n  @override\n  String get autoBackupConflict => '自動バックアップは一度に一つしか開始できません';\n\n  @override\n  String get autoConnect => '自動接続';\n\n  @override\n  String get autoRun => '自動実行';\n\n  @override\n  String get autoUpdateHomeWidget => 'ホームウィジェットを自動更新';\n\n  @override\n  String get availableTabs => '利用可能なタブ';\n\n  @override\n  String get backupEncrypted => 'バックアップは暗号化されています';\n\n  @override\n  String get backupNotEncrypted => 'バックアップは暗号化されていません';\n\n  @override\n  String get backupPassword => 'バックアップパスワード';\n\n  @override\n  String get backupPasswordRemoved => 'バックアップパスワードが削除されました';\n\n  @override\n  String get backupPasswordSet => 'バックアップパスワードが設定されました';\n\n  @override\n  String get backupPasswordTip =>\n      'バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。';\n\n  @override\n  String get backupPasswordWrong => 'バックアップパスワードが間違っています';\n\n  @override\n  String get backupTip => 'エクスポートされたデータはパスワードで暗号化できます。 \\n適切に保管してください。';\n\n  @override\n  String get backupVersionNotMatch => 'バックアップバージョンが一致しないため、復元できません';\n\n  @override\n  String get bgRun => 'バックグラウンド実行';\n\n  @override\n  String get bgRunTip =>\n      'このスイッチはプログラムがバックグラウンドで実行を試みることを意味しますが、実際にバックグラウンドで実行できるかどうかは、権限が有効になっているかに依存します。AOSPベースのAndroid ROMでは、このアプリの「バッテリー最適化」をオフにしてください。MIUIでは、省エネモードを「無制限」に変更してください。';\n\n  @override\n  String get clearAllStatsContent => 'すべてのサーバー接続統計を削除してもよろしいですか？この操作は元に戻せません。';\n\n  @override\n  String get clearAllStatsTitle => 'すべての統計をクリア';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return 'サーバー\\\"$serverName\\\"の接続統計を削除してもよろしいですか？この操作は元に戻せません。';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return '$serverNameの統計をクリア';\n  }\n\n  @override\n  String get clearThisServerStats => 'このサーバーの統計をクリア';\n\n  @override\n  String get compactDatabase => 'データベースを圧縮';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'データベースサイズ: $size\\n\\nこれにより、ファイルサイズを小さくするためにデータベースが再編成されます。データは削除されません。';\n  }\n\n  @override\n  String get closeAfterSave => '保存して閉じる';\n\n  @override\n  String get collapseUITip => 'UIの長いリストをデフォルトで折りたたむかどうか';\n\n  @override\n  String get connectionDetails => '接続の詳細';\n\n  @override\n  String get connectionStats => '接続統計';\n\n  @override\n  String get connectionStatsDesc => 'サーバー接続成功率と履歴を表示';\n\n  @override\n  String get containerTrySudoTip =>\n      '例：アプリ内でユーザーをaaaに設定しているが、Dockerがrootユーザーでインストールされている場合、このオプションを有効にする必要があります';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Dockerにアクセスするにはsudoパスワードが必要です。パスワードを入力してください。';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'sudoパスワードが正しくないか、許可されていません。再試行してください。';\n\n  @override\n  String get convert => '変換';\n\n  @override\n  String get copyPath => 'パスをコピー';\n\n  @override\n  String get cpuViewAsProgressTip => '各CPUの使用率をプログレスバースタイルで表示する（旧スタイル）';\n\n  @override\n  String get cursorType => 'カーソルタイプ';\n\n  @override\n  String get customCmd => 'カスタムコマンド';\n\n  @override\n  String get customCmdHint => '\\\"コマンド名\\\": \\\"コマンド\\\"';\n\n  @override\n  String get deleteServers => 'サーバーを一括削除';\n\n  @override\n  String get desktopTerminalTip => 'SSHセッションを起動する際に使用されるターミナルエミュレーターを開くコマンド。';\n\n  @override\n  String get dirEmpty => 'フォルダーが空であることを確認してください';\n\n  @override\n  String get discoverSshServers => 'SSHサーバーの発見';\n\n  @override\n  String get discoveryFailed => '発見に失敗';\n\n  @override\n  String get discoverySettings => '発見設定';\n\n  @override\n  String get discoverySummary => '発見の概要';\n\n  @override\n  String get diskHealth => 'ディスクの健康状態';\n\n  @override\n  String get diskIgnorePath => '無視されたディスクパス';\n\n  @override\n  String get displayCpuIndex => 'CPUインデックスを表示する';\n\n  @override\n  String dl2Local(Object fileName) {\n    return '$fileNameをローカルにダウンロードしますか？';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      '実行中のコンテナがありません。\\nこれは次の理由による可能性があります：\\n- Dockerのインストールユーザーとアプリ内の設定されたユーザー名が異なる\\n- 環境変数DOCKER_HOSTが正しく読み込まれていない。ターミナルで`echo \\$DOCKER_HOST`を実行して取得できます。';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '合計$countイメージ';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Dockerがインストールされていません';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount個が実行中、$stoppedCount個が停止中';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count個のコンテナが実行中';\n  }\n\n  @override\n  String get doubleColumnMode => 'ダブルカラムモード';\n\n  @override\n  String get doubleColumnTip =>\n      'このオプションは機能を有効にするだけで、実際に有効にできるかどうかはデバイスの幅に依存します';\n\n  @override\n  String get editVirtKeys => '仮想キーを編集';\n\n  @override\n  String get editorHighlightTip =>\n      '現在のコードハイライトのパフォーマンスはかなり悪いため、改善するために無効にすることを選択できます。';\n\n  @override\n  String get enableMdns => 'mDNSを有効化';\n\n  @override\n  String get enableMdnsDesc => 'mDNS/BonjourでSSHサービスを発見';\n\n  @override\n  String get envVars => '環境変数';\n\n  @override\n  String get experimentalFeature => '実験的な機能';\n\n  @override\n  String get extraArgs => '追加引数';\n\n  @override\n  String get fallbackSshDest => 'フォールバックSSH宛先';\n\n  @override\n  String get fdroidReleaseTip =>\n      'このアプリをF-Droidからダウンロードした場合、このオプションをオフにすることをお勧めします。';\n\n  @override\n  String get fgService => 'フォアグラウンドサービス';\n\n  @override\n  String get fgServiceTip =>\n      '有効にすると、一部の機種でクラッシュする可能性があります。無効にすると、一部の機種でバックグラウンドでのSSH接続を維持できなくなる可能性があります。システム設定でServerBoxの通知権限、バックグラウンド実行、自己起動を許可してください。';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'ファイル \\'$file\\' は大きすぎます \\'$size\\'、$sizeMax を超えています';\n  }\n\n  @override\n  String get finishedAt => '完了時刻';\n\n  @override\n  String get followSystem => 'システムに従う';\n\n  @override\n  String get fontSize => 'フォントサイズ';\n\n  @override\n  String get fullScreen => 'フルスクリーンモード';\n\n  @override\n  String get fullScreenJitter => 'フルスクリーンモードのジッター';\n\n  @override\n  String get fullScreenJitterHelp => '焼き付き防止';\n\n  @override\n  String get fullScreenTip =>\n      'デバイスが横向きに回転したときにフルスクリーンモードを有効にしますか？このオプションはサーバータブにのみ適用されます。';\n\n  @override\n  String get goBackQ => '戻りますか？';\n\n  @override\n  String get goto => '移動';\n\n  @override\n  String get hideTitleBar => 'タイトルバーを非表示にする';\n\n  @override\n  String get highlight => 'コードハイライト';\n\n  @override\n  String get homeTabs => 'ホームタブ';\n\n  @override\n  String get homeTabsCustomizeDesc => 'ホームページに表示するタブとその順序をカスタマイズします';\n\n  @override\n  String get homeWidgetUrlConfig => 'ホームウィジェットURL設定';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'リクエスト失敗、ステータスコード: $code';\n  }\n\n  @override\n  String get ignoreCert => '証明書を無視する';\n\n  @override\n  String get image => 'イメージ';\n\n  @override\n  String get imagesList => 'イメージリスト';\n\n  @override\n  String get installDockerWithUrl =>\n      '最初に https://docs.docker.com/engine/install dockerをインストールしてください';\n\n  @override\n  String get invalid => '無効';\n\n  @override\n  String get invalidHostFormat =>\n      'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.';\n\n  @override\n  String get jumpServer => 'ジャンプサーバー';\n\n  @override\n  String get keepForeground => 'アプリを前面に保ってください！';\n\n  @override\n  String get keepStatusWhenErr => 'エラー時に前回のサーバーステータスを保持';\n\n  @override\n  String get keepStatusWhenErrTip => 'スクリプトの実行エラーに限ります';\n\n  @override\n  String get keyAuth => 'キー認証';\n\n  @override\n  String get lastFailure => '最後の失敗';\n\n  @override\n  String get lastSuccess => '最後の成功';\n\n  @override\n  String get letterCache => '文字キャッシング';\n\n  @override\n  String get letterCacheTip => '無効にすることを推奨しますが、無効にした後はCJK文字を入力することができなくなります。';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return '$myGithubによって❤️で作成済み';\n  }\n\n  @override\n  String get max => '最大';\n\n  @override\n  String get maxConcurrency => '最大同時実行数';\n\n  @override\n  String get maxRetryCount => 'サーバーの再接続試行回数';\n\n  @override\n  String get maxRetryCountEqual0 => '無限に再試行します';\n\n  @override\n  String get min => '最小';\n\n  @override\n  String get more => 'もっと';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      '有効にする：サーバータブの各カードの下に表示されます。無効にする：サーバーの詳細ページの上部に表示されます。';\n\n  @override\n  String get needHomeDir =>\n      'Synologyユーザーの場合は、[こちらをご覧ください](https://kb.synology.com/DSM/tutorial/user_enable_home_service)。他のシステムのユーザーは、ホームディレクトリの作成方法を検索する必要があります。';\n\n  @override\n  String get needRestart => 'アプリを再起動する必要があります';\n\n  @override\n  String get netViewType => 'ネットワークビュータイプ';\n\n  @override\n  String get newContainer => '新しいコンテナを作成';\n\n  @override\n  String get noConnectionStatsData => '接続統計データがありません';\n\n  @override\n  String get noLineChart => '折れ線グラフを使用しない';\n\n  @override\n  String get noLineChartForCpu => 'CPUに折れ線グラフを使わない';\n\n  @override\n  String get noPrivateKeyTip => '秘密鍵が存在しません。削除されたか、設定ミスがある可能性があります。';\n\n  @override\n  String get noPromptAgain => '再度確認しない';\n\n  @override\n  String get onServerDetailPage => 'サーバーの詳細ページで';\n\n  @override\n  String get onlyOneLine => '一行のみ表示（スクロール可能）';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 => 'コア数が8より大きい場合にのみ有効';\n\n  @override\n  String get openLastPath => '最後のパスを開く';\n\n  @override\n  String get openLastPathTip => '異なるサーバーには異なる記録があり、記録されているのは退出時のパスです';\n\n  @override\n  String get parseContainerStatsTip => 'Dockerの使用状況の解析は比較的遅いです';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$size の $percent%';\n  }\n\n  @override\n  String get permission => '権限';\n\n  @override\n  String get pingInputIP => '対象のIPまたはドメインを入力してください';\n\n  @override\n  String get pingNoServer => 'Pingに使用するサーバーがありません\\nサーバータブでサーバーを追加してから再試行してください';\n\n  @override\n  String get plugInType => '挿入タイプ';\n\n  @override\n  String get preferDiskAmount => 'ディスク容量を優先的に表示';\n\n  @override\n  String get privateKey => '秘密鍵';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return '秘密鍵 [$keyId] が見つかりません。';\n  }\n\n  @override\n  String get pushToken => 'プッシュトークン';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'オプションを有効にすることは推奨されません、セキュリティリスクに注意してください！PVEのデフォルト証明書を使用している場合は、このオプションを有効にする必要があります。';\n\n  @override\n  String get pveLoginFailed =>\n      'ログインに失敗しました。Linux PAMログインのためにサーバー構成からのユーザー名/パスワードで認証できません。';\n\n  @override\n  String get pveVersionLow => 'この機能は現在テスト段階にあり、PVE 8+でのみテストされています。ご利用の際は慎重に。';\n\n  @override\n  String get read => '読み取り';\n\n  @override\n  String get recentConnections => '最近の接続';\n\n  @override\n  String get rememberPwdInMem => 'メモリにパスワードを記憶する';\n\n  @override\n  String get rememberPwdInMemTip => 'コンテナ、一時停止などに使用されます。';\n\n  @override\n  String get rememberWindowSize => 'ウィンドウサイズを記憶する';\n\n  @override\n  String get remotePath => 'リモートパス';\n\n  @override\n  String get result => '結果';\n\n  @override\n  String get rotateAngel => '回転角度';\n\n  @override\n  String get sameIdServerExist => '同じIDのサーバーが既に存在します';\n\n  @override\n  String get save => '保存';\n\n  @override\n  String get second => '秒';\n\n  @override\n  String get serverDetailOrder => '詳細ページのウィジェット順序';\n\n  @override\n  String get serverFuncBtns => 'サーバー機能ボタン';\n\n  @override\n  String get serverOrder => 'サーバー順序';\n\n  @override\n  String get serverTabRequired => 'サーバータブは削除できません';\n\n  @override\n  String get sftpDlPrepare => 'サーバーへの接続を準備中...';\n\n  @override\n  String get sftpEditorTip =>\n      '空の場合は、アプリ内蔵のファイルエディタを使用します。値がある場合は、リモートサーバーのエディタ（例：`vim`）を使用します（`EDITOR` に従って自動検出することをお勧めします）。';\n\n  @override\n  String get sftpRmrDirSummary => 'SFTPで`rm -r`を使用してフォルダーを削除';\n\n  @override\n  String get sftpSSHConnected => 'SFTPに接続されました...';\n\n  @override\n  String get sftpShowFoldersFirst => 'フォルダーを先に表示';\n\n  @override\n  String get showDistLogo => 'ディストリビューションのロゴを表示';\n\n  @override\n  String get size => 'サイズ';\n\n  @override\n  String get softWrap => 'ソフトラップ';\n\n  @override\n  String get specifyDev => 'デバイスを指定';\n\n  @override\n  String get specifyDevTip =>\n      '例えば、ネットワークトラフィック統計はデフォルトですべてのデバイスに対するものです。ここで特定のデバイスを指定できます。';\n\n  @override\n  String get speed => '速度';\n\n  @override\n  String spentTime(Object time) {\n    return '費した時間: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'すべてのサーバーがすでに存在します（$duplicateCount個の重複が見つかりました）';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount個の重複がスキップされます';\n  }\n\n  @override\n  String get sshConfigFound => 'システムにSSH設定が見つかりました。';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return '$totalCount個のサーバーが見つかりました';\n  }\n\n  @override\n  String get sshConfigImport => 'SSH設定のインポート';\n\n  @override\n  String get sshConfigImportHelp => 'インポートできるのは基本情報のみです。例：IP/ポート。';\n\n  @override\n  String get sshConfigImportPermission =>\n      '~/.ssh/configを読み取ってサーバー設定を自動的にインポートする権限を与えますか？';\n\n  @override\n  String get sshConfigImportTip => '初回サーバー作成時に~/.ssh/configの読み取りを促す';\n\n  @override\n  String sshConfigImported(Object count) {\n    return 'SSH設定から$count個のサーバーをインポートしました';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return '$serverName の SSH ホスト鍵が変更されました。このサーバーを信頼できる場合のみ続行してください。';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'フィンガープリント (MD5 Base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'フィンガープリント (MD5 16進): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'SSH ホストキーの種類';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return '$serverName から新しい SSH ホスト鍵を受信しました。信頼する前にフィンガープリントを確認してください。';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return '保存済みフィンガープリント: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect => 'SSH設定ファイルを手動で選択しますか？';\n\n  @override\n  String get sshConfigNoServers => 'SSH設定でサーバーが見つかりませんでした';\n\n  @override\n  String get sshConfigPermissionDenied => 'macOSの権限により、SSH設定ファイルにアクセスできません。';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount個のサーバーがインポートされます';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。';\n\n  @override\n  String sshTip(Object url) {\n    return 'この機能は現在テスト段階にあります。\\n\\n問題がある場合は、$urlでフィードバックしてください。';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff => '仮想キーの自動オフ';\n\n  @override\n  String get stat => '統計';\n\n  @override\n  String get supportFmtArgs => '以下のフォーマット引数がサポートされています：';\n\n  @override\n  String get suspendTip => 'suspend機能はroot権限とsystemdのサポートが必要です。';\n\n  @override\n  String switchTo(Object val) {\n    return '$valに切り替える';\n  }\n\n  @override\n  String get syncTip => '再起動が必要な場合があります。一部の変更はその後に有効になります。';\n\n  @override\n  String get system => 'システム';\n\n  @override\n  String get tag => 'タグ';\n\n  @override\n  String get tapToStartDiscovery => '検索ボタンをタップしてネットワーク上のSSHサーバーを発見';\n\n  @override\n  String get termFontSizeTip =>\n      'この設定は端末のサイズ（幅と高さ）に影響します。現在のセッションのフォントサイズを調整するために、端末ページを拡大縮小できます。';\n\n  @override\n  String get textScaler => 'テキストスケーラー';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100%（デフォルトサイズ）。サーバーページの一部のテキストにのみ適用されます。変更をお勧めしません。';\n\n  @override\n  String get time => '時間';\n\n  @override\n  String get times => '回';\n\n  @override\n  String get trySudo => 'sudoを試みる';\n\n  @override\n  String get unknown => '不明';\n\n  @override\n  String get unkownConvertMode => '未知の変換モード';\n\n  @override\n  String get update => '更新';\n\n  @override\n  String get updateIntervalEqual0 =>\n      '0に設定すると、サーバーの状態は自動的に更新されず、CPU使用率も計算できません。';\n\n  @override\n  String get updateServerStatusInterval => 'サーバー状態の更新間隔';\n\n  @override\n  String get upsideDown => '上下逆転';\n\n  @override\n  String get useCdn => 'CDNの使用';\n\n  @override\n  String get useCdnTip => '中国以外のユーザーにはCDNの使用が推奨されています。ご利用しますか？';\n\n  @override\n  String get useNoPwd => 'パスワードなしで使用します';\n\n  @override\n  String get usePodmanByDefault => 'デフォルトでPodmanを使用';\n\n  @override\n  String get used => '使用済み';\n\n  @override\n  String get view => 'ビュー';\n\n  @override\n  String get viewDetails => '詳細を表示';\n\n  @override\n  String get viewErr => 'エラーを表示';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      '端末に選択された文字がある場合は、選択された文字をクリップボードにコピーします。そうでない場合は、クリップボードの内容を端末に貼り付けます。';\n\n  @override\n  String get virtKeyHelpIME => 'キーボードのオン/オフ';\n\n  @override\n  String get virtKeyHelpSFTP => '現在のパスでSFTPを開く。';\n\n  @override\n  String get waitConnection => '接続の確立を待ってください';\n\n  @override\n  String get wakeLock => '起動を保つ';\n\n  @override\n  String get watchNotPaired => 'ペアリングされたApple Watchがありません';\n\n  @override\n  String get webdavSettingEmpty => 'Webdavの設定が空です';\n\n  @override\n  String get whenOpenApp => 'アプリを開くとき';\n\n  @override\n  String get wolTip => 'WOL（Wake-on-LAN）を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。';\n\n  @override\n  String get write => '書き込み';\n\n  @override\n  String get writeScriptFailTip =>\n      'スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。';\n\n  @override\n  String get writeScriptTip =>\n      'サーバーへの接続後、システムステータスを監視するスクリプトが `~/.config/server_box` \\n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Podman Docker エミュレーションが検出されました。設定で Podman に切り替えてください。';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_ko.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Korean (`ko`).\nclass AppLocalizationsKo extends AppLocalizations {\n  AppLocalizationsKo([String locale = 'ko']) : super(locale);\n\n  @override\n  String get aboutThanks => '다음 참여자분들에게 감사드립니다.';\n\n  @override\n  String get acceptBeta => '베타 버전 업데이트 수락';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      '현재 개인 키가 없습니다. 시스템 기본 키(~/.ssh/id_rsa)를 추가하시겠습니까?';\n\n  @override\n  String get added2List => '작업 목록에 추가되었습니다';\n\n  @override\n  String get addr => '주소';\n\n  @override\n  String get alreadyLastDir => '이미 마지막 디렉토리입니다.';\n\n  @override\n  String get askAi => 'AI에게 질문';\n\n  @override\n  String get askAiApiKey => 'API 키';\n\n  @override\n  String get askAiAwaitingResponse => 'AI 응답 대기 중...';\n\n  @override\n  String get askAiBaseUrl => '기본 URL';\n\n  @override\n  String get askAiCommandInserted => '명령어가 터미널에 삽입되었습니다';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return '설정에서 $fields을(를) 구성해 주세요.';\n  }\n\n  @override\n  String get askAiConfirmExecute => '실행 전 확인';\n\n  @override\n  String get askAiConversation => 'AI 대화';\n\n  @override\n  String get askAiDisclaimer => 'AI가 부정확할 수 있습니다. 적용 전에 주의 깊게 검토해 주세요.';\n\n  @override\n  String get askAiFollowUpHint => '추가 질문하기...';\n\n  @override\n  String get askAiInsertTerminal => '터미널에 삽입';\n\n  @override\n  String get askAiNoResponse => '응답 없음';\n\n  @override\n  String get askAiRecommendedCommand => 'AI 추천 명령어';\n\n  @override\n  String get askAiSelectedContent => '선택한 내용';\n\n  @override\n  String get askAiUsageHint => 'SSH 터미널에서 사용';\n\n  @override\n  String get atLeastOneTab => '최소 하나의 탭을 선택해야 합니다';\n\n  @override\n  String get authFailTip => '인증에 실패했습니다. 자격 증명이 올바른지 확인해 주세요.';\n\n  @override\n  String get autoBackupConflict => '자동 백업은 한 번에 하나만 활성화할 수 있습니다.';\n\n  @override\n  String get autoConnect => '자동 연결';\n\n  @override\n  String get autoRun => '자동 실행';\n\n  @override\n  String get autoUpdateHomeWidget => '홈 위젯 자동 업데이트';\n\n  @override\n  String get availableTabs => '사용 가능한 탭';\n\n  @override\n  String get backupEncrypted => '백업이 암호화되어 있습니다';\n\n  @override\n  String get backupNotEncrypted => '백업이 암호화되어 있지 않습니다';\n\n  @override\n  String get backupPassword => '백업 비밀번호';\n\n  @override\n  String get backupPasswordRemoved => '백업 비밀번호가 제거되었습니다';\n\n  @override\n  String get backupPasswordSet => '백업 비밀번호가 설정되었습니다';\n\n  @override\n  String get backupPasswordTip =>\n      '백업 파일을 암호화하기 위한 비밀번호를 설정하세요. 암호화를 비활성화하려면 비워 두세요.';\n\n  @override\n  String get backupPasswordWrong => '백업 비밀번호가 올바르지 않습니다';\n\n  @override\n  String get backupTip => '내보낸 데이터는 비밀번호로 암호화할 수 있습니다.\\n안전하게 보관해 주세요.';\n\n  @override\n  String get backupVersionNotMatch => '백업 버전이 일치하지 않습니다.';\n\n  @override\n  String get bgRun => '백그라운드 실행';\n\n  @override\n  String get bgRunTip =>\n      '이 스위치는 프로그램이 백그라운드에서 실행을 시도한다는 의미입니다. 실제 백그라운드 실행 가능 여부는 권한 활성화 여부에 따라 다릅니다. AOSP 기반 Android ROM의 경우, 이 앱의 \\\"배터리 최적화\\\"를 비활성화해 주세요. MIUI / HyperOS의 경우, 절전 정책을 \\\"무제한\\\"으로 변경해 주세요.';\n\n  @override\n  String get clearAllStatsContent => '모든 서버 연결 통계를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.';\n\n  @override\n  String get clearAllStatsTitle => '모든 통계 삭제';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return '서버 \\\"$serverName\\\"의 연결 통계를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return '$serverName 통계 삭제';\n  }\n\n  @override\n  String get clearThisServerStats => '이 서버의 통계 삭제';\n\n  @override\n  String get compactDatabase => '데이터베이스 압축';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return '데이터베이스 크기: $size\\n\\n데이터베이스를 재구성하여 파일 크기를 줄입니다. 데이터는 삭제되지 않습니다.';\n  }\n\n  @override\n  String get closeAfterSave => '저장 후 닫기';\n\n  @override\n  String get collapseUITip => 'UI의 긴 목록을 기본적으로 접을지 여부';\n\n  @override\n  String get connectionDetails => '연결 상세 정보';\n\n  @override\n  String get connectionStats => '연결 통계';\n\n  @override\n  String get connectionStatsDesc => '서버 연결 성공률 및 기록 보기';\n\n  @override\n  String get containerTrySudoTip =>\n      '예: 앱에서 사용자를 aaa로 설정했지만 Docker가 root 사용자로 설치된 경우, 이 옵션을 활성화해야 합니다.';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Docker에 접근하려면 sudo 비밀번호가 필요합니다. 비밀번호를 입력해 주세요.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'sudo 비밀번호가 올바르지 않거나 허용되지 않습니다. 다시 시도해 주세요.';\n\n  @override\n  String get convert => '변환';\n\n  @override\n  String get copyPath => '경로 복사';\n\n  @override\n  String get cpuViewAsProgressTip => '각 CPU 사용률을 프로그레스 바 형태로 표시합니다 (이전 스타일)';\n\n  @override\n  String get cursorType => '커서 유형';\n\n  @override\n  String get customCmd => '사용자 정의 명령어';\n\n  @override\n  String get customCmdHint => '\\\"명령어 이름\\\": \\\"명령어\\\"';\n\n  @override\n  String get deleteServers => '서버 일괄 삭제';\n\n  @override\n  String get desktopTerminalTip => 'SSH 세션을 시작할 때 사용할 터미널 에뮬레이터를 여는 명령어입니다.';\n\n  @override\n  String get dirEmpty => '폴더가 비어 있는지 확인해 주세요.';\n\n  @override\n  String get discoverSshServers => 'SSH 서버 검색';\n\n  @override\n  String get discoveryFailed => '검색 실패';\n\n  @override\n  String get discoverySettings => '검색 설정';\n\n  @override\n  String get discoverySummary => '검색 요약';\n\n  @override\n  String get diskHealth => '디스크 상태';\n\n  @override\n  String get diskIgnorePath => '디스크 무시 경로';\n\n  @override\n  String get displayCpuIndex => 'CPU 인덱스 표시';\n\n  @override\n  String dl2Local(Object fileName) {\n    return '$fileName을(를) 로컬에 다운로드하시겠습니까?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      '실행 중인 컨테이너가 없습니다.\\n다음과 같은 원인이 있을 수 있습니다:\\n- Docker 설치 사용자와 앱에 설정된 사용자 이름이 다릅니다.\\n- 환경 변수 DOCKER_HOST가 올바르게 읽히지 않았습니다. 터미널에서 `echo \\$DOCKER_HOST`를 실행하여 확인할 수 있습니다.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '이미지 $count개';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker가 설치되어 있지 않습니다';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount개 실행 중, $stoppedCount개 중지됨.';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '컨테이너 $count개 실행 중.';\n  }\n\n  @override\n  String get doubleColumnMode => '이중 열 모드';\n\n  @override\n  String get doubleColumnTip => '이 옵션은 기능만 활성화하며, 실제 적용 여부는 기기의 너비에 따라 다릅니다';\n\n  @override\n  String get editVirtKeys => '가상 키 편집';\n\n  @override\n  String get editorHighlightTip =>\n      '현재 코드 하이라이팅 성능이 이상적이지 않습니다. 성능 향상을 위해 선택적으로 끌 수 있습니다.';\n\n  @override\n  String get enableMdns => 'mDNS 활성화';\n\n  @override\n  String get enableMdnsDesc => 'mDNS/Bonjour를 사용하여 SSH 서비스 검색';\n\n  @override\n  String get envVars => '환경 변수';\n\n  @override\n  String get experimentalFeature => '실험적 기능';\n\n  @override\n  String get extraArgs => '추가 인수';\n\n  @override\n  String get fallbackSshDest => '대체 SSH 대상';\n\n  @override\n  String get fdroidReleaseTip => 'F-Droid에서 이 앱을 다운로드한 경우, 이 옵션을 끄는 것을 권장합니다.';\n\n  @override\n  String get fgService => '포그라운드 서비스';\n\n  @override\n  String get fgServiceTip =>\n      '활성화 시 일부 기기에서 충돌이 발생할 수 있습니다. 비활성화 시 일부 기기에서 백그라운드 SSH 연결을 유지할 수 없을 수 있습니다. 시스템 설정에서 ServerBox 알림 권한, 백그라운드 실행, 자체 깨우기를 허용해 주세요.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return '파일 \\'$file\\'이(가) 너무 큽니다. 크기 $size, 최대 $sizeMax';\n  }\n\n  @override\n  String get finishedAt => '완료 시각';\n\n  @override\n  String get followSystem => '시스템 설정 따르기';\n\n  @override\n  String get fontSize => '글꼴 크기';\n\n  @override\n  String get fullScreen => '전체 화면 모드';\n\n  @override\n  String get fullScreenJitter => '전체 화면 지터';\n\n  @override\n  String get fullScreenJitterHelp => '화면 번인 방지';\n\n  @override\n  String get fullScreenTip =>\n      '기기를 가로 모드로 회전할 때 전체 화면 모드를 활성화하시겠습니까? 이 옵션은 서버 탭에만 적용됩니다.';\n\n  @override\n  String get goBackQ => '돌아가시겠습니까?';\n\n  @override\n  String get goto => '이동';\n\n  @override\n  String get hideTitleBar => '제목 표시줄 숨기기';\n\n  @override\n  String get highlight => '코드 하이라이팅';\n\n  @override\n  String get homeTabs => '홈 탭';\n\n  @override\n  String get homeTabsCustomizeDesc => '홈 페이지에 표시할 탭과 순서를 사용자 지정합니다';\n\n  @override\n  String get homeWidgetUrlConfig => '홈 위젯 URL 설정';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return '요청 실패, 상태 코드: $code';\n  }\n\n  @override\n  String get ignoreCert => '인증서 무시';\n\n  @override\n  String get image => '이미지';\n\n  @override\n  String get imagesList => '이미지 목록';\n\n  @override\n  String get installDockerWithUrl =>\n      '먼저 https://docs.docker.com/engine/install 에서 Docker를 설치해 주세요.';\n\n  @override\n  String get invalid => '유효하지 않음';\n\n  @override\n  String get invalidHostFormat => '잘못된 호스트 형식입니다. IPv4, IPv6, 도메인 문자만 허용됩니다.';\n\n  @override\n  String get jumpServer => '점프 서버';\n\n  @override\n  String get keepForeground => '앱을 포그라운드에 유지해 주세요!';\n\n  @override\n  String get keepStatusWhenErr => '마지막 서버 상태 유지';\n\n  @override\n  String get keepStatusWhenErrTip => '스크립트 실행 중 오류가 발생한 경우에만';\n\n  @override\n  String get keyAuth => '키 인증';\n\n  @override\n  String get lastFailure => '마지막 실패';\n\n  @override\n  String get lastSuccess => '마지막 성공';\n\n  @override\n  String get letterCache => '문자 캐싱';\n\n  @override\n  String get letterCacheTip => '비활성화를 권장하지만, 비활성화하면 CJK 문자를 입력할 수 없습니다.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return '$myGithub이(가) ❤️로 만들었습니다';\n  }\n\n  @override\n  String get max => '최대';\n\n  @override\n  String get maxConcurrency => '최대 동시 실행 수';\n\n  @override\n  String get maxRetryCount => '서버 재연결 횟수';\n\n  @override\n  String get maxRetryCountEqual0 => '계속해서 재시도합니다.';\n\n  @override\n  String get min => '최소';\n\n  @override\n  String get more => '더 보기';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      '켜기: 서버 탭의 각 카드 아래에 표시됩니다. 끄기: 서버 상세 페이지 상단에 표시됩니다.';\n\n  @override\n  String get needHomeDir =>\n      'Synology 사용자는 [여기를 참조하세요](https://kb.synology.com/DSM/tutorial/user_enable_home_service). 다른 시스템 사용자는 홈 디렉토리 생성 방법을 검색해야 합니다.';\n\n  @override\n  String get needRestart => '앱을 다시 시작해야 합니다';\n\n  @override\n  String get netViewType => '네트워크 뷰 유형';\n\n  @override\n  String get newContainer => '새 컨테이너';\n\n  @override\n  String get noConnectionStatsData => '연결 통계 데이터가 없습니다';\n\n  @override\n  String get noLineChart => '꺾은선 그래프 사용 안 함';\n\n  @override\n  String get noLineChartForCpu => 'CPU에 꺾은선 그래프 사용 안 함';\n\n  @override\n  String get noPrivateKeyTip => '개인 키가 존재하지 않습니다. 삭제되었거나 설정 오류일 수 있습니다.';\n\n  @override\n  String get noPromptAgain => '다시 묻지 않기';\n\n  @override\n  String get onServerDetailPage => '서버 상세 페이지에서';\n\n  @override\n  String get onlyOneLine => '한 줄로만 표시 (스크롤 가능)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 => '코어 수가 8개 이상일 때만 작동합니다';\n\n  @override\n  String get openLastPath => '마지막 경로 열기';\n\n  @override\n  String get openLastPathTip => '서버마다 다른 기록이 있으며, 기록은 종료 시의 경로입니다';\n\n  @override\n  String get parseContainerStatsTip => 'Docker 점유 상태 파싱이 비교적 느립니다.';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$size의 $percent%';\n  }\n\n  @override\n  String get permission => '권한';\n\n  @override\n  String get pingInputIP => '대상 IP / 도메인을 입력해 주세요.';\n\n  @override\n  String get pingNoServer => 'Ping할 서버가 없습니다.\\n서버 탭에서 서버를 추가해 주세요.';\n\n  @override\n  String get plugInType => '삽입 유형';\n\n  @override\n  String get preferDiskAmount => '디스크 용량 우선 표시';\n\n  @override\n  String get privateKey => '개인 키';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return '개인 키 [$keyId]을(를) 찾을 수 없습니다.';\n  }\n\n  @override\n  String get pushToken => '푸시 토큰';\n\n  @override\n  String get pveIgnoreCertTip =>\n      '활성화를 권장하지 않습니다. 보안 위험에 주의하세요! PVE 기본 인증서를 사용하는 경우 이 옵션을 활성화해야 합니다.';\n\n  @override\n  String get pveLoginFailed =>\n      '로그인에 실패했습니다. 서버 설정의 사용자 이름/비밀번호로 Linux PAM 인증을 할 수 없습니다.';\n\n  @override\n  String get pveVersionLow =>\n      '이 기능은 현재 테스트 단계이며 PVE 8+에서만 테스트되었습니다. 주의하여 사용해 주세요.';\n\n  @override\n  String get read => '읽기';\n\n  @override\n  String get recentConnections => '최근 연결';\n\n  @override\n  String get rememberPwdInMem => '메모리에 비밀번호 저장';\n\n  @override\n  String get rememberPwdInMemTip => '컨테이너, 일시 중지 등에 사용됩니다.';\n\n  @override\n  String get rememberWindowSize => '창 크기 기억';\n\n  @override\n  String get remotePath => '원격 경로';\n\n  @override\n  String get result => '결과';\n\n  @override\n  String get rotateAngel => '회전 각도';\n\n  @override\n  String get sameIdServerExist => '동일한 ID의 서버가 이미 존재합니다';\n\n  @override\n  String get save => '저장';\n\n  @override\n  String get second => '초';\n\n  @override\n  String get serverDetailOrder => '상세 페이지 위젯 순서';\n\n  @override\n  String get serverFuncBtns => '서버 기능 버튼';\n\n  @override\n  String get serverOrder => '서버 순서';\n\n  @override\n  String get serverTabRequired => '서버 탭은 제거할 수 없습니다';\n\n  @override\n  String get sftpDlPrepare => '연결 준비 중...';\n\n  @override\n  String get sftpEditorTip =>\n      '비어 있으면 앱 내장 파일 편집기를 사용합니다. 값이 있으면 원격 서버의 편집기(예: `vim`)를 사용합니다 (`EDITOR`에 따라 자동 감지 권장).';\n\n  @override\n  String get sftpRmrDirSummary => 'SFTP에서 `rm -r`을 사용하여 폴더를 삭제합니다.';\n\n  @override\n  String get sftpSSHConnected => 'SFTP 연결됨';\n\n  @override\n  String get sftpShowFoldersFirst => '폴더 우선 표시';\n\n  @override\n  String get showDistLogo => '배포판 로고 표시';\n\n  @override\n  String get size => '크기';\n\n  @override\n  String get softWrap => '자동 줄바꿈';\n\n  @override\n  String get specifyDev => '장치 지정';\n\n  @override\n  String get specifyDevTip =>\n      '예를 들어, 네트워크 트래픽 통계는 기본적으로 모든 장치를 대상으로 합니다. 여기서 특정 장치를 지정할 수 있습니다.';\n\n  @override\n  String get speed => '속도';\n\n  @override\n  String spentTime(Object time) {\n    return '소요 시간: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return '모든 서버가 이미 존재합니다 (중복 $duplicateCount개 발견)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '중복 $duplicateCount개가 건너뛰어집니다';\n  }\n\n  @override\n  String get sshConfigFound => '시스템에서 SSH 설정을 발견했습니다.';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return '서버 $totalCount개를 발견했습니다';\n  }\n\n  @override\n  String get sshConfigImport => 'SSH 설정 가져오기';\n\n  @override\n  String get sshConfigImportHelp => '기본 정보만 가져올 수 있습니다. 예: IP/포트.';\n\n  @override\n  String get sshConfigImportPermission =>\n      '~/.ssh/config를 읽고 서버 설정을 자동으로 가져올 수 있는 권한을 부여하시겠습니까?';\n\n  @override\n  String get sshConfigImportTip => '첫 서버 생성 시 ~/.ssh/config 읽기 안내';\n\n  @override\n  String sshConfigImported(Object count) {\n    return 'SSH 설정에서 서버 $count개를 가져왔습니다';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return '$serverName의 SSH 호스트 키가 변경되었습니다. 이 서버를 신뢰하는 경우에만 계속 진행하세요.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return '지문 (MD5 base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return '지문 (MD5 hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'SSH 호스트 키 유형';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return '$serverName에서 새 SSH 호스트 키를 수신했습니다. 신뢰하기 전에 지문을 확인해 주세요.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return '저장된 지문: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect => 'SSH 설정 파일을 수동으로 선택하시겠습니까?';\n\n  @override\n  String get sshConfigNoServers => 'SSH 설정에서 서버를 찾을 수 없습니다';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'macOS 권한으로 인해 SSH 설정 파일에 접근할 수 없습니다.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '서버 $importCount개가 가져오기됩니다';\n  }\n\n  @override\n  String get sshTermHelp =>\n      '터미널이 스크롤 가능할 때, 가로로 드래그하면 텍스트를 선택할 수 있습니다. 키보드 버튼을 누르면 키보드가 켜지거나 꺼집니다. 파일 아이콘은 현재 경로를 SFTP로 엽니다. 클립보드 버튼은 텍스트가 선택되어 있을 때 내용을 복사하고, 텍스트가 선택되어 있지 않고 클립보드에 내용이 있을 때 터미널에 붙여넣습니다. 코드 아이콘은 코드 스니펫을 터미널에 붙여넣고 실행합니다.';\n\n  @override\n  String sshTip(Object url) {\n    return '이 기능은 현재 실험 단계입니다.\\n\\n$url에서 버그를 보고하거나 개발에 참여해 주세요.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff => '가상 키 자동 전환';\n\n  @override\n  String get stat => '통계';\n\n  @override\n  String get supportFmtArgs => '다음 형식 매개변수가 지원됩니다:';\n\n  @override\n  String get suspendTip => '일시 중지 기능은 root 권한과 systemd 지원이 필요합니다.';\n\n  @override\n  String switchTo(Object val) {\n    return '$val(으)로 전환';\n  }\n\n  @override\n  String get syncTip => '일부 변경 사항은 재시작해야 적용될 수 있습니다.';\n\n  @override\n  String get system => '시스템';\n\n  @override\n  String get tag => '태그';\n\n  @override\n  String get tapToStartDiscovery => '검색 버튼을 눌러 네트워크에서 SSH 서버를 검색하세요';\n\n  @override\n  String get termFontSizeTip =>\n      '이 설정은 터미널 크기(너비 및 높이)에 영향을 줍니다. 현재 세션의 글꼴 크기를 조정하려면 터미널 페이지에서 확대/축소할 수 있습니다.';\n\n  @override\n  String get textScaler => '텍스트 배율';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (원래 크기), 서버 페이지의 일부 글꼴에만 적용되며 변경을 권장하지 않습니다.';\n\n  @override\n  String get time => '시간';\n\n  @override\n  String get times => '회';\n\n  @override\n  String get trySudo => 'sudo 사용 시도';\n\n  @override\n  String get unknown => '알 수 없음';\n\n  @override\n  String get unkownConvertMode => '알 수 없는 변환 모드';\n\n  @override\n  String get update => '업데이트';\n\n  @override\n  String get updateIntervalEqual0 =>\n      '0으로 설정하면 자동으로 업데이트되지 않습니다.\\nCPU 상태를 계산할 수 없습니다.';\n\n  @override\n  String get updateServerStatusInterval => '서버 상태 업데이트 간격';\n\n  @override\n  String get upsideDown => '상하 반전';\n\n  @override\n  String get useCdn => 'CDN 사용';\n\n  @override\n  String get useCdnTip => '중국 이외 사용자에게는 CDN 사용을 권장합니다. 사용하시겠습니까?';\n\n  @override\n  String get useNoPwd => '비밀번호를 사용하지 않습니다';\n\n  @override\n  String get usePodmanByDefault => '기본적으로 Podman 사용';\n\n  @override\n  String get used => '사용됨';\n\n  @override\n  String get view => '보기';\n\n  @override\n  String get viewDetails => '상세 보기';\n\n  @override\n  String get viewErr => '오류 보기';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      '터미널에 선택된 텍스트가 있으면 클립보드에 복사하고, 없으면 클립보드 내용을 터미널에 붙여넣습니다.';\n\n  @override\n  String get virtKeyHelpIME => '키보드 켜기/끄기';\n\n  @override\n  String get virtKeyHelpSFTP => '현재 디렉토리를 SFTP로 열기.';\n\n  @override\n  String get waitConnection => '연결이 설정될 때까지 기다려 주세요.';\n\n  @override\n  String get wakeLock => '화면 깨우기 유지';\n\n  @override\n  String get watchNotPaired => '페어링된 Apple Watch가 없습니다';\n\n  @override\n  String get webdavSettingEmpty => 'WebDav 설정이 비어 있습니다';\n\n  @override\n  String get whenOpenApp => '앱을 열 때';\n\n  @override\n  String get wolTip => 'WOL (Wake-on-LAN)을 설정하면 서버에 연결할 때마다 WOL 요청이 전송됩니다.';\n\n  @override\n  String get write => '쓰기';\n\n  @override\n  String get writeScriptFailTip =>\n      '스크립트 작성에 실패했습니다. 권한이 부족하거나 디렉토리가 존재하지 않을 수 있습니다.';\n\n  @override\n  String get writeScriptTip =>\n      '서버 연결 후 시스템 상태를 모니터링하기 위한 스크립트가 `~/.config/server_box` \\n | `/tmp/server_box`에 작성됩니다. 스크립트 내용을 확인할 수 있습니다.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub 저장소';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Podman Docker 에뮬레이션이 감지되었습니다. 설정에서 Podman으로 전환해 주세요.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_nl.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Dutch Flemish (`nl`).\nclass AppLocalizationsNl extends AppLocalizations {\n  AppLocalizationsNl([String locale = 'nl']) : super(locale);\n\n  @override\n  String get aboutThanks =>\n      'Met dank aan de volgende mensen die hebben deelgenomen aan.';\n\n  @override\n  String get acceptBeta => 'Accepteer testversie-updates';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'Er is momenteel geen privésleutel, wilt u degene toevoegen die bij het systeem wordt geleverd (~/.ssh/id_rsa)?';\n\n  @override\n  String get added2List => 'Toegevoegd aan takenlijst';\n\n  @override\n  String get addr => 'Adres';\n\n  @override\n  String get alreadyLastDir => 'Al in de laatst gebruikte map.';\n\n  @override\n  String get askAi => 'AI vragen';\n\n  @override\n  String get askAiApiKey => 'API-sleutel';\n\n  @override\n  String get askAiAwaitingResponse => 'Wachten op AI-reactie...';\n\n  @override\n  String get askAiBaseUrl => 'Basis-URL';\n\n  @override\n  String get askAiCommandInserted => 'Commando in terminal ingevoegd';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Configureer $fields in de instellingen.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Bevestigen voor uitvoeren';\n\n  @override\n  String get askAiConversation => 'AI-gesprek';\n\n  @override\n  String get askAiDisclaimer => 'AI kan fouten maken. Gebruik het zorgvuldig.';\n\n  @override\n  String get askAiFollowUpHint => 'Stel een vervolgvraag...';\n\n  @override\n  String get askAiInsertTerminal => 'In terminal invoegen';\n\n  @override\n  String get askAiNoResponse => 'Geen reactie';\n\n  @override\n  String get askAiRecommendedCommand => 'Door AI voorgestelde opdracht';\n\n  @override\n  String get askAiSelectedContent => 'Geselecteerde inhoud';\n\n  @override\n  String get askAiUsageHint => 'Gebruikt in de SSH-terminal';\n\n  @override\n  String get atLeastOneTab =>\n      'Er moet minimaal één tabblad worden geselecteerd';\n\n  @override\n  String get authFailTip =>\n      'Authenticatie mislukt, controleer of het wachtwoord/sleutel/host/gebruiker, enz., incorrect zijn.';\n\n  @override\n  String get autoBackupConflict =>\n      'Er kan slechts één automatische back-up tegelijk worden ingeschakeld.';\n\n  @override\n  String get autoConnect => 'Automatisch verbinden';\n\n  @override\n  String get autoRun => 'Automatisch uitvoeren';\n\n  @override\n  String get autoUpdateHomeWidget => 'Automatische update van home-widget';\n\n  @override\n  String get availableTabs => 'Beschikbare tabbladen';\n\n  @override\n  String get backupEncrypted => 'Back-up is versleuteld';\n\n  @override\n  String get backupNotEncrypted => 'Back-up is niet versleuteld';\n\n  @override\n  String get backupPassword => 'Back-up wachtwoord';\n\n  @override\n  String get backupPasswordRemoved => 'Back-up wachtwoord verwijderd';\n\n  @override\n  String get backupPasswordSet => 'Back-up wachtwoord ingesteld';\n\n  @override\n  String get backupPasswordTip =>\n      'Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.';\n\n  @override\n  String get backupPasswordWrong => 'Onjuist back-up wachtwoord';\n\n  @override\n  String get backupTip =>\n      'De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \\nBewaar deze aub veilig.';\n\n  @override\n  String get backupVersionNotMatch => 'Back-upversie komt niet overeen.';\n\n  @override\n  String get bgRun => 'Uitvoeren op de achtergrond';\n\n  @override\n  String get bgRunTip =>\n      'Deze schakelaar betekent alleen dat het programma zal proberen op de achtergrond uit te voeren, of het in de achtergrond kan worden uitgevoerd, hangt af van of de toestemming is ingeschakeld of niet. Voor native Android, schakel \\\"Batterijoptimalisatie\\\" uit in deze app, en voor miui, wijzig de energiebesparingsbeleid naar \\\"Onbeperkt\\\".';\n\n  @override\n  String get clearAllStatsContent =>\n      'Weet u zeker dat u alle serververbindingsstatistieken wilt wissen? Deze actie kan niet ongedaan worden gemaakt.';\n\n  @override\n  String get clearAllStatsTitle => 'Alle statistieken wissen';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return 'Weet u zeker dat u de verbindingsstatistieken voor server \\\"$serverName\\\" wilt wissen? Deze actie kan niet ongedaan worden gemaakt.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return 'Statistieken van $serverName wissen';\n  }\n\n  @override\n  String get clearThisServerStats => 'Statistieken van deze server wissen';\n\n  @override\n  String get compactDatabase => 'Database comprimeren';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Databasgrootte: $size\\n\\nDit zal de database opnieuw organiseren om de bestandsgrootte te verkleinen. Geen gegevens worden verwijderd.';\n  }\n\n  @override\n  String get closeAfterSave => 'Opslaan en sluiten';\n\n  @override\n  String get collapseUITip =>\n      'Of lange lijsten in de UI standaard moeten worden ingeklapt';\n\n  @override\n  String get connectionDetails => 'Verbindingsdetails';\n\n  @override\n  String get connectionStats => 'Verbindingsstatistieken';\n\n  @override\n  String get connectionStatsDesc =>\n      'Bekijk server verbindingssucces ratio en geschiedenis';\n\n  @override\n  String get containerTrySudoTip =>\n      'Bijvoorbeeld: in de app is de gebruiker ingesteld op aaa, maar Docker is geïnstalleerd onder de rootgebruiker. In dit geval moet u deze optie inschakelen.';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Een sudo-wachtwoord is vereist om toegang te krijgen tot Docker. Voer uw wachtwoord in.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'Het sudo-wachtwoord is onjuist of niet toegestaan. Probeer het opnieuw.';\n\n  @override\n  String get convert => 'Converteren';\n\n  @override\n  String get copyPath => 'Pad kopiëren';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Toon het gebruik van elke CPU in een voortgangsbalkstijl (oude stijl)';\n\n  @override\n  String get cursorType => 'Cursortype';\n\n  @override\n  String get customCmd => 'Aangepaste opdrachten';\n\n  @override\n  String get customCmdHint => '\\\"Opdrachtnaam\\\": \\\"Opdracht\\\"';\n\n  @override\n  String get deleteServers => 'Servers batchgewijs verwijderen';\n\n  @override\n  String get desktopTerminalTip =>\n      'Opdracht die wordt gebruikt om de terminalemulator te openen bij het starten van SSH-sessies.';\n\n  @override\n  String get dirEmpty => 'Zorg ervoor dat de map leeg is.';\n\n  @override\n  String get discoverSshServers => 'SSH-servers ontdekken';\n\n  @override\n  String get discoveryFailed => 'Ontdekking mislukt';\n\n  @override\n  String get discoverySettings => 'Ontdekkingsinstellingen';\n\n  @override\n  String get discoverySummary => 'Ontdekkingssamenvatting';\n\n  @override\n  String get diskHealth => 'Schijfgezondheid';\n\n  @override\n  String get diskIgnorePath => 'Pad negeren voor schijf';\n\n  @override\n  String get displayCpuIndex => 'Toon de CPU-index';\n\n  @override\n  String dl2Local(Object fileName) {\n    return 'Download $fileName naar lokaal?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'Er zijn geen actieve containers.\\nDit kan komen doordat:\\n- De Docker-installatiegebruiker niet overeenkomt met de gebruikersnaam die is geconfigureerd binnen de app.\\n- De omgevingsvariabele DOCKER_HOST is niet correct gelezen. U kunt deze krijgen door `echo \\$DOCKER_HOST` in de terminal uit te voeren.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '$count afbeeldingen';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker niet geïnstalleerd';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount actief, $stoppedCount container gestopt.';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count container actief.';\n  }\n\n  @override\n  String get doubleColumnMode => 'Dubbele kolommodus';\n\n  @override\n  String get doubleColumnTip =>\n      'Deze optie schakelt alleen de functie in, of deze daadwerkelijk kan worden ingeschakeld, hangt af van de breedte van het apparaat';\n\n  @override\n  String get editVirtKeys => 'Virtuele toetsen bewerken';\n\n  @override\n  String get editorHighlightTip =>\n      'De huidige codehighlighting-prestaties zijn slechter en kunnen optioneel worden uitgeschakeld om te verbeteren.';\n\n  @override\n  String get enableMdns => 'mDNS inschakelen';\n\n  @override\n  String get enableMdnsDesc =>\n      'Gebruik mDNS/Bonjour om SSH-services te ontdekken';\n\n  @override\n  String get envVars => 'Omgevingsvariabele';\n\n  @override\n  String get experimentalFeature => 'Experimentele functie';\n\n  @override\n  String get extraArgs => 'Extra argumenten';\n\n  @override\n  String get fallbackSshDest => 'Fallback SSH-bestemming';\n\n  @override\n  String get fdroidReleaseTip =>\n      'Als u deze app van F-Droid heeft gedownload, wordt aanbevolen deze optie uit te schakelen.';\n\n  @override\n  String get fgService => 'Voorgrondservice';\n\n  @override\n  String get fgServiceTip =>\n      'Na het inschakelen kunnen sommige apparaatmodellen crashen. Uitschakelen kan ertoe leiden dat sommige modellen SSH-verbindingen niet op de achtergrond kunnen behouden. Sta ServerBox notificatierechten, achtergronduitvoering en zelf-ontwaken toe in systeeminstellingen.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'Bestand \\'$file\\' te groot $size, max $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Voltooid om';\n\n  @override\n  String get followSystem => 'Volg systeem';\n\n  @override\n  String get fontSize => 'Lettergrootte';\n\n  @override\n  String get fullScreen => 'Volledig schermmodus';\n\n  @override\n  String get fullScreenJitter => 'Volledig scherm trilling';\n\n  @override\n  String get fullScreenJitterHelp => 'Om inbranden van het scherm te voorkomen';\n\n  @override\n  String get fullScreenTip =>\n      'Moet de volledig schermmodus worden ingeschakeld wanneer het apparaat naar de liggende modus wordt gedraaid? Deze optie is alleen van toepassing op het servertabblad.';\n\n  @override\n  String get goBackQ => 'Terug gaan?';\n\n  @override\n  String get goto => 'Ga naar';\n\n  @override\n  String get hideTitleBar => 'Titelbalk verbergen';\n\n  @override\n  String get highlight => 'Code-highlight';\n\n  @override\n  String get homeTabs => 'Home-tabbladen';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Pas aan welke tabbladen op de startpagina worden weergegeven en hun volgorde';\n\n  @override\n  String get homeWidgetUrlConfig => 'Home-widget-url configureren';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'verzoek mislukt, statuscode: $code';\n  }\n\n  @override\n  String get ignoreCert => 'Certificaat negeren';\n\n  @override\n  String get image => 'Afbeelding';\n\n  @override\n  String get imagesList => 'Lijst met afbeeldingen';\n\n  @override\n  String get installDockerWithUrl =>\n      'Installeer eerst docker via https://docs.docker.com/engine/install.';\n\n  @override\n  String get invalid => 'Ongeldig';\n\n  @override\n  String get invalidHostFormat =>\n      'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.';\n\n  @override\n  String get jumpServer => 'Spring naar server';\n\n  @override\n  String get keepForeground => 'Houd de app op de voorgrond!';\n\n  @override\n  String get keepStatusWhenErr => 'Behoud de laatste serverstatus';\n\n  @override\n  String get keepStatusWhenErrTip =>\n      'Alleen in geval van een fout tijdens de scriptuitvoering';\n\n  @override\n  String get keyAuth => 'Sleutelauthenticatie';\n\n  @override\n  String get lastFailure => 'Laatst gefaald';\n\n  @override\n  String get lastSuccess => 'Laatst succesvol';\n\n  @override\n  String get letterCache => 'Lettercaching';\n\n  @override\n  String get letterCacheTip =>\n      'Aanbevolen om uit te schakelen, maar na het uitschakelen is het niet mogelijk om CJK-tekens in te voeren.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return 'Gemaakt met ❤️ door $myGithub';\n  }\n\n  @override\n  String get max => 'max';\n\n  @override\n  String get maxConcurrency => 'Maximale gelijktijdigheid';\n\n  @override\n  String get maxRetryCount => 'Aantal serverherverbindingen';\n\n  @override\n  String get maxRetryCountEqual0 => 'Zal opnieuw blijven proberen.';\n\n  @override\n  String get min => 'min';\n\n  @override\n  String get more => 'Meer';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'Aan: kan worden weergegeven onder elke kaart op de Server-tabbladpagina. Uit: kan worden weergegeven bovenaan de Serverdetails-pagina.';\n\n  @override\n  String get needHomeDir =>\n      'Als u een Synology-gebruiker bent, [zie hier](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Gebruikers van andere systemen moeten zoeken hoe ze een home directory kunnen creëren.';\n\n  @override\n  String get needRestart => 'App moet opnieuw worden gestart';\n\n  @override\n  String get netViewType => 'Netweergavetype';\n\n  @override\n  String get newContainer => 'Nieuwe container';\n\n  @override\n  String get noConnectionStatsData => 'Geen verbindingsstatistiekgegevens';\n\n  @override\n  String get noLineChart => 'lijndiagrammen gebruiken';\n\n  @override\n  String get noLineChartForCpu => 'Gebruik geen lijndiagrammen voor CPU';\n\n  @override\n  String get noPrivateKeyTip =>\n      'De privésleutel bestaat niet, deze is mogelijk verwijderd of er is een configuratiefout.';\n\n  @override\n  String get noPromptAgain => 'Niet meer vragen';\n\n  @override\n  String get onServerDetailPage => 'Op serverdetailspagina';\n\n  @override\n  String get onlyOneLine => 'Alleen als één regel weergeven (scrollbaar)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 =>\n      'Alleen effectief wanneer het aantal cores > 8';\n\n  @override\n  String get openLastPath => 'Open het laatste pad';\n\n  @override\n  String get openLastPathTip =>\n      'Verschillende servers hebben verschillende logs, en de log is het pad naar de uitgang';\n\n  @override\n  String get parseContainerStatsTip =>\n      'Het parsen van de bezettingsstatus van Docker is relatief langzaam.';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$percent% van $size';\n  }\n\n  @override\n  String get permission => 'Machtigingen';\n\n  @override\n  String get pingInputIP => 'Voer een doel-IP / domein in.';\n\n  @override\n  String get pingNoServer =>\n      'Geen server om te pingen.\\nVoeg een server toe in het servertabblad.';\n\n  @override\n  String get plugInType => 'Invoegingstype';\n\n  @override\n  String get preferDiskAmount =>\n      'Geef de schijfcapaciteit prioriteit bij weergave';\n\n  @override\n  String get privateKey => 'Privésleutel';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'Privésleutel [$keyId] niet gevonden.';\n  }\n\n  @override\n  String get pushToken => 'Push-token';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'Niet aanbevolen om in te schakelen, let op beveiligingsrisico\\'s! Als u de standaardcertificaat van PVE gebruikt, moet u deze optie inschakelen.';\n\n  @override\n  String get pveLoginFailed =>\n      'Aanmelden mislukt. Kan niet authenticeren met gebruikersnaam/wachtwoord van serverconfiguratie voor Linux PAM-login.';\n\n  @override\n  String get pveVersionLow =>\n      'Deze functie bevindt zich momenteel in de testfase en is alleen getest op PVE 8+. Gebruik het met voorzichtigheid.';\n\n  @override\n  String get read => 'Lezen';\n\n  @override\n  String get recentConnections => 'Recente verbindingen';\n\n  @override\n  String get rememberPwdInMem => 'Wachtwoord onthouden in geheugen';\n\n  @override\n  String get rememberPwdInMemTip =>\n      'Gebruikt voor containers, opschorting, enz.';\n\n  @override\n  String get rememberWindowSize => 'Venstergrootte onthouden';\n\n  @override\n  String get remotePath => 'Extern pad';\n\n  @override\n  String get result => 'Resultaat';\n\n  @override\n  String get rotateAngel => 'Rotatiehoek';\n\n  @override\n  String get sameIdServerExist => 'Er bestaat al een server met dezelfde ID';\n\n  @override\n  String get save => 'Opslaan';\n\n  @override\n  String get second => 's';\n\n  @override\n  String get serverDetailOrder => 'Volgorde van widget op detailpagina';\n\n  @override\n  String get serverFuncBtns => 'Server functieknoppen';\n\n  @override\n  String get serverOrder => 'Servervolgorde';\n\n  @override\n  String get serverTabRequired => 'Servertabblad kan niet worden verwijderd';\n\n  @override\n  String get sftpDlPrepare => 'Voorbereiden om verbinding te maken...';\n\n  @override\n  String get sftpEditorTip =>\n      'Indien leeg, gebruik de ingebouwde bestandseditor van de app. Indien een waarde aanwezig is, gebruik de editor van de externe server, bijvoorbeeld `vim` (aanbevolen om automatisch te detecteren volgens `EDITOR`).';\n\n  @override\n  String get sftpRmrDirSummary =>\n      'Gebruik `rm -r` om een map te verwijderen in SFTP.';\n\n  @override\n  String get sftpSSHConnected => 'SFTP Verbonden';\n\n  @override\n  String get sftpShowFoldersFirst => 'Mappen eerst weergeven';\n\n  @override\n  String get showDistLogo => 'Distributielogo weergeven';\n\n  @override\n  String get size => 'Grootte';\n\n  @override\n  String get softWrap => 'Zachte wrap';\n\n  @override\n  String get specifyDev => 'Apparaat specificeren';\n\n  @override\n  String get specifyDevTip =>\n      'Bijvoorbeeld, netwerkverkeersstatistieken zijn standaard voor alle apparaten. Hier kunt u een specifiek apparaat opgeven.';\n\n  @override\n  String get speed => 'Snelheid';\n\n  @override\n  String spentTime(Object time) {\n    return 'Gebruikte tijd: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'Alle servers bestaan al ($duplicateCount duplicaten gevonden)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount duplicaten worden overgeslagen';\n  }\n\n  @override\n  String get sshConfigFound =>\n      'We hebben SSH-configuratie op uw systeem gevonden';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return '$totalCount servers gevonden';\n  }\n\n  @override\n  String get sshConfigImport => 'SSH Configuratie Importeren';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Alleen basisinformatie kan worden geïmporteerd, bijvoorbeeld: IP/Poort.';\n\n  @override\n  String get sshConfigImportPermission =>\n      'Wilt u toestemming geven om ~/.ssh/config te lezen en automatisch serverinstellingen te importeren?';\n\n  @override\n  String get sshConfigImportTip =>\n      'Prompt om ~/.ssh/config te lezen bij het aanmaken van de eerste server';\n\n  @override\n  String sshConfigImported(Object count) {\n    return '$count servers geïmporteerd uit SSH-configuratie';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return 'De SSH-hostsleutel voor $serverName is gewijzigd. Ga alleen verder als u deze server vertrouwt.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Vingerafdruk (MD5 Base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Vingerafdruk (MD5 hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'Type SSH-hostsleutel';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return 'Er is een nieuwe SSH-hostsleutel ontvangen van $serverName. Controleer de vingerafdruk voordat u vertrouwt.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Opgeslagen vingerafdruk: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      'Wilt u het SSH-configuratiebestand handmatig selecteren?';\n\n  @override\n  String get sshConfigNoServers => 'Geen servers gevonden in SSH-configuratie';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'Kan geen toegang krijgen tot SSH-configuratiebestand vanwege macOS-rechten.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount servers worden geïmporteerd';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'Wanneer het terminal scrollbaar is, kan horizontaal slepen tekst selecteren. Klikken op de toetsenbordknop schakelt het toetsenbord aan/uit. Het bestandsicoon opent de huidige pad SFTP. De klembordknop kopieert de inhoud wanneer tekst is geselecteerd en plakt inhoud van het klembord in de terminal wanneer geen tekst is geselecteerd en er inhoud op het klembord staat. Het code-icoon plakt codefragmenten in de terminal en voert ze uit.';\n\n  @override\n  String sshTip(Object url) {\n    return 'Deze functie bevindt zich momenteel in de experimentele fase.\\n\\nMeld alstublieft bugs op $url of sluit je aan bij onze ontwikkeling.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff =>\n      'Automatisch schakelen van virtuele toetsen';\n\n  @override\n  String get stat => 'Statistieken';\n\n  @override\n  String get supportFmtArgs =>\n      'De volgende opmaakparameters worden ondersteund:';\n\n  @override\n  String get suspendTip =>\n      'De opschortfunctie vereist rootrechten en systemd-ondersteuning.';\n\n  @override\n  String switchTo(Object val) {\n    return 'Overschakelen naar $val';\n  }\n\n  @override\n  String get syncTip =>\n      'Een herstart kan nodig zijn voor sommige wijzigingen om van kracht te worden.';\n\n  @override\n  String get system => 'Systeem';\n\n  @override\n  String get tag => 'Labels';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Tik op de zoekknop om SSH-servers op uw netwerk te ontdekken';\n\n  @override\n  String get termFontSizeTip =>\n      'Deze instelling heeft invloed op de terminalgrootte (breedte en hoogte). U kunt inzoomen op de terminalpagina om de lettergrootte van de huidige sessie aan te passen.';\n\n  @override\n  String get textScaler => 'Tekstschaler';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (oorspronkelijke grootte), werkt alleen op het gedeelte van de serverpagina van het lettertype, niet aanbevolen om te wijzigen.';\n\n  @override\n  String get time => 'Tijd';\n\n  @override\n  String get times => 'Keer';\n\n  @override\n  String get trySudo => 'Probeer sudo te gebruiken';\n\n  @override\n  String get unknown => 'Onbekend';\n\n  @override\n  String get unkownConvertMode => 'Onbekende conversiemodus';\n\n  @override\n  String get update => 'Bijwerken';\n\n  @override\n  String get updateIntervalEqual0 =>\n      'Het staat op 0, het zal niet automatisch bijwerken\\nCPU status kan niet berekend worden.';\n\n  @override\n  String get updateServerStatusInterval =>\n      'Interne server status bijwerking interval';\n\n  @override\n  String get upsideDown => 'Ondersteboven';\n\n  @override\n  String get useCdn => 'Gebruikt CDN';\n\n  @override\n  String get useCdnTip =>\n      'Niet-chinese gebruikers worden aangeraden om deze CDN te gebruiken. Wil je dat?';\n\n  @override\n  String get useNoPwd => 'Er zal geen wachtwoord gebruikt worden';\n\n  @override\n  String get usePodmanByDefault => 'Valt terug op Podman';\n\n  @override\n  String get used => 'Gebruikt';\n\n  @override\n  String get view => 'Weergave';\n\n  @override\n  String get viewDetails => 'Details bekijken';\n\n  @override\n  String get viewErr => 'Zie foutmelding';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'Kopiëren naar het klembord als de geselecteerde terminal niet leeg is, anders de inhoud van het klembord plakken in de terminal.';\n\n  @override\n  String get virtKeyHelpIME => 'Toetsenbord aan/uit zetten';\n\n  @override\n  String get virtKeyHelpSFTP => 'Huidige map openen in SFTP.';\n\n  @override\n  String get waitConnection =>\n      'Wacht alstublieft tot de verbinding tot stand is gebracht.';\n\n  @override\n  String get wakeLock => 'Wakker houden';\n\n  @override\n  String get watchNotPaired => 'Geen gekoppelde Apple Watch';\n\n  @override\n  String get webdavSettingEmpty => 'Webdav-instelling is leeg';\n\n  @override\n  String get whenOpenApp => 'Bij het openen van de app';\n\n  @override\n  String get wolTip =>\n      'Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.';\n\n  @override\n  String get write => 'Schrijven';\n\n  @override\n  String get writeScriptFailTip =>\n      'Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.';\n\n  @override\n  String get writeScriptTip =>\n      'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \\n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Podman Docker-emulatie gedetecteerd. Schakel over naar Podman in de instellingen.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_pt.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Portuguese (`pt`).\nclass AppLocalizationsPt extends AppLocalizations {\n  AppLocalizationsPt([String locale = 'pt']) : super(locale);\n\n  @override\n  String get aboutThanks => 'Agradecimentos a todos os participantes.';\n\n  @override\n  String get acceptBeta => 'Aceitar atualizações da versão de teste';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'Atualmente, não há nenhuma chave privada. Gostaria de adicionar a chave do sistema (~/.ssh/id_rsa)?';\n\n  @override\n  String get added2List => 'Adicionado à lista de tarefas';\n\n  @override\n  String get addr => 'Endereço';\n\n  @override\n  String get alreadyLastDir => 'Já é o diretório mais alto';\n\n  @override\n  String get askAi => 'Perguntar à IA';\n\n  @override\n  String get askAiApiKey => 'Chave de API';\n\n  @override\n  String get askAiAwaitingResponse => 'Aguardando resposta da IA...';\n\n  @override\n  String get askAiBaseUrl => 'URL base';\n\n  @override\n  String get askAiCommandInserted => 'Comando inserido no terminal';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Configure $fields nas configurações.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Confirmar antes de executar';\n\n  @override\n  String get askAiConversation => 'Conversa com a IA';\n\n  @override\n  String get askAiDisclaimer => 'A IA pode errar. Use com cautela.';\n\n  @override\n  String get askAiFollowUpHint => 'Faça uma pergunta adicional...';\n\n  @override\n  String get askAiInsertTerminal => 'Inserir no terminal';\n\n  @override\n  String get askAiNoResponse => 'Sem resposta';\n\n  @override\n  String get askAiRecommendedCommand => 'Comando sugerido pela IA';\n\n  @override\n  String get askAiSelectedContent => 'Conteúdo selecionado';\n\n  @override\n  String get askAiUsageHint => 'Usado no terminal SSH';\n\n  @override\n  String get atLeastOneTab => 'Pelo menos uma aba deve ser selecionada';\n\n  @override\n  String get authFailTip =>\n      'Autenticação falhou, por favor verifique se a senha/chave/host/usuário, etc., estão incorretos.';\n\n  @override\n  String get autoBackupConflict =>\n      'Apenas um backup automático pode ser ativado por vez';\n\n  @override\n  String get autoConnect => 'Conexão automática';\n\n  @override\n  String get autoRun => 'Execução automática';\n\n  @override\n  String get autoUpdateHomeWidget =>\n      'Atualização automática do widget da tela inicial';\n\n  @override\n  String get availableTabs => 'Abas disponíveis';\n\n  @override\n  String get backupEncrypted => 'Backup está criptografado';\n\n  @override\n  String get backupNotEncrypted => 'Backup não está criptografado';\n\n  @override\n  String get backupPassword => 'Senha de backup';\n\n  @override\n  String get backupPasswordRemoved => 'Senha de backup removida';\n\n  @override\n  String get backupPasswordSet => 'Senha de backup definida';\n\n  @override\n  String get backupPasswordTip =>\n      'Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.';\n\n  @override\n  String get backupPasswordWrong => 'Senha de backup incorreta';\n\n  @override\n  String get backupTip =>\n      'Os dados exportados podem ser criptografados com senha. \\nPor favor, guarde-os com segurança.';\n\n  @override\n  String get backupVersionNotMatch =>\n      'Versão de backup não compatível, não é possível restaurar';\n\n  @override\n  String get bgRun => 'Execução em segundo plano';\n\n  @override\n  String get bgRunTip =>\n      'Este interruptor indica que o programa tentará rodar em segundo plano, mas a capacidade de fazer isso depende das permissões concedidas. No Android nativo, desative a \\'Otimização de bateria\\' para este app, no MIUI, altere a estratégia de economia de energia para \\'Sem restrições\\'.';\n\n  @override\n  String get clearAllStatsContent =>\n      'Tem certeza de que deseja limpar todas as estatísticas de conexão do servidor? Esta ação não pode ser desfeita.';\n\n  @override\n  String get clearAllStatsTitle => 'Limpar todas as estatísticas';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return 'Tem certeza de que deseja limpar as estatísticas de conexão para o servidor \\\"$serverName\\\"? Esta ação não pode ser desfeita.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return 'Limpar estatísticas de $serverName';\n  }\n\n  @override\n  String get clearThisServerStats => 'Limpar estatísticas deste servidor';\n\n  @override\n  String get compactDatabase => 'Compactar banco de dados';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Tamanho do banco de dados: $size\\n\\nIsso reorganizará o banco de dados para reduzir o tamanho do arquivo. Nenhum dado será excluído.';\n  }\n\n  @override\n  String get closeAfterSave => 'Salvar e fechar';\n\n  @override\n  String get collapseUITip => 'Deve colapsar listas longas na UI por padrão?';\n\n  @override\n  String get connectionDetails => 'Detalhes da conexão';\n\n  @override\n  String get connectionStats => 'Estatísticas de conexão';\n\n  @override\n  String get connectionStatsDesc =>\n      'Ver taxa de sucesso de conexão do servidor e histórico';\n\n  @override\n  String get containerTrySudoTip =>\n      'Por exemplo: se o usuário for definido como aaa dentro do app, mas o Docker estiver instalado sob o usuário root, esta opção precisará ser ativada';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'É necessária uma senha sudo para acessar o Docker. Por favor, insira sua senha.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'A senha sudo está incorreta ou não é permitida. Por favor, tente novamente.';\n\n  @override\n  String get convert => 'Converter';\n\n  @override\n  String get copyPath => 'Copiar caminho';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Exiba a taxa de uso de cada CPU em estilo de barra de progresso (estilo antigo)';\n\n  @override\n  String get cursorType => 'Tipo de cursor';\n\n  @override\n  String get customCmd => 'Comandos personalizados';\n\n  @override\n  String get customCmdHint => '\\\"Nome do comando\\\": \\\"Comando\\\"';\n\n  @override\n  String get deleteServers => 'Excluir servidores em lote';\n\n  @override\n  String get desktopTerminalTip =>\n      'Comando usado para abrir o emulador de terminal ao iniciar sessões SSH.';\n\n  @override\n  String get dirEmpty => 'Certifique-se de que a pasta está vazia';\n\n  @override\n  String get discoverSshServers => 'Descobrir servidores SSH';\n\n  @override\n  String get discoveryFailed => 'Descoberta falhou';\n\n  @override\n  String get discoverySettings => 'Configurações de descoberta';\n\n  @override\n  String get discoverySummary => 'Resumo da descoberta';\n\n  @override\n  String get diskHealth => 'Saúde do disco';\n\n  @override\n  String get diskIgnorePath => 'Caminhos de disco ignorados';\n\n  @override\n  String get displayCpuIndex => 'Exiba o índice de CPU';\n\n  @override\n  String dl2Local(Object fileName) {\n    return 'Baixar $fileName para o local?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'Não há contêineres em execução.\\nIsso pode ser porque:\\n- O usuário que instalou o Docker difere do usuário configurado no app\\n- A variável de ambiente DOCKER_HOST não foi lida corretamente. Você pode verificar isso executando `echo \\$DOCKER_HOST` no terminal.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return 'Total de $count imagens';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker não instalado';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount em execução, $stoppedCount parados';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count contêiner(es) em execução';\n  }\n\n  @override\n  String get doubleColumnMode => 'Modo de coluna dupla';\n\n  @override\n  String get doubleColumnTip =>\n      'Esta opção apenas ativa a funcionalidade, se ela será ativada depende também da largura do dispositivo';\n\n  @override\n  String get editVirtKeys => 'Editar teclas virtuais';\n\n  @override\n  String get editorHighlightTip =>\n      'O desempenho do destaque de código atualmente é ruim, pode optar por desativá-lo para melhorar.';\n\n  @override\n  String get enableMdns => 'Ativar mDNS';\n\n  @override\n  String get enableMdnsDesc => 'Usar mDNS/Bonjour para descobrir serviços SSH';\n\n  @override\n  String get envVars => 'Variável de ambiente';\n\n  @override\n  String get experimentalFeature => 'Recurso experimental';\n\n  @override\n  String get extraArgs => 'Argumentos extras';\n\n  @override\n  String get fallbackSshDest => 'Destino SSH de fallback';\n\n  @override\n  String get fdroidReleaseTip =>\n      'Se você baixou este aplicativo do F-Droid, é recomendado desativar esta opção.';\n\n  @override\n  String get fgService => 'Serviço em primeiro plano';\n\n  @override\n  String get fgServiceTip =>\n      'Após ativar, alguns modelos de dispositivos podem travar. Desativar pode fazer com que alguns modelos não consigam manter conexões SSH em segundo plano. Por favor, permita as permissões de notificação do ServerBox, execução em segundo plano e auto-despertar nas configurações do sistema.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'Arquivo \\'$file\\' muito grande \\'$size\\', excedendo $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Terminado em';\n\n  @override\n  String get followSystem => 'Seguir sistema';\n\n  @override\n  String get fontSize => 'Tamanho da fonte';\n\n  @override\n  String get fullScreen => 'Modo tela cheia';\n\n  @override\n  String get fullScreenJitter => 'Tremulação em tela cheia';\n\n  @override\n  String get fullScreenJitterHelp => 'Prevenir burn-in de tela';\n\n  @override\n  String get fullScreenTip =>\n      'Deve ser ativado o modo de tela cheia quando o dispositivo é girado para o modo paisagem? Esta opção aplica-se apenas à aba do servidor.';\n\n  @override\n  String get goBackQ => 'Voltar?';\n\n  @override\n  String get goto => 'Ir para';\n\n  @override\n  String get hideTitleBar => 'Ocultar barra de título';\n\n  @override\n  String get highlight => 'Destaque de código';\n\n  @override\n  String get homeTabs => 'Abas iniciais';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Personalize quais abas aparecem na página inicial e sua ordem';\n\n  @override\n  String get homeWidgetUrlConfig =>\n      'Configuração de URL do widget da tela inicial';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'Falha na solicitação, código de status: $code';\n  }\n\n  @override\n  String get ignoreCert => 'Ignorar certificado';\n\n  @override\n  String get image => 'Imagem';\n\n  @override\n  String get imagesList => 'Lista de imagens';\n\n  @override\n  String get installDockerWithUrl =>\n      'Por favor, instale o Docker primeiro em https://docs.docker.com/engine/install';\n\n  @override\n  String get invalid => 'Inválido';\n\n  @override\n  String get invalidHostFormat =>\n      'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.';\n\n  @override\n  String get jumpServer => 'Servidor de salto';\n\n  @override\n  String get keepForeground => 'Por favor, mantenha o app em primeiro plano!';\n\n  @override\n  String get keepStatusWhenErr => 'Manter o status anterior do servidor';\n\n  @override\n  String get keepStatusWhenErrTip => 'Limitado a erros de execução de scripts';\n\n  @override\n  String get keyAuth => 'Autenticação por chave';\n\n  @override\n  String get lastFailure => 'Última falha';\n\n  @override\n  String get lastSuccess => 'Último sucesso';\n\n  @override\n  String get letterCache => 'Cache de letras';\n\n  @override\n  String get letterCacheTip =>\n      'Recomendado desativar, mas após desativar, será impossível inserir caracteres CJK.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return 'Feito com ❤️ por $myGithub';\n  }\n\n  @override\n  String get max => 'Máximo';\n\n  @override\n  String get maxConcurrency => 'Concorrência máxima';\n\n  @override\n  String get maxRetryCount =>\n      'Número de tentativas de reconexão com o servidor';\n\n  @override\n  String get maxRetryCountEqual0 => 'Irá tentar indefinidamente';\n\n  @override\n  String get min => 'Mínimo';\n\n  @override\n  String get more => 'Mais';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'Ativado: Mostra abaixo de cada cartão na aba do servidor. Desativado: Mostra no topo da página de detalhes do servidor.';\n\n  @override\n  String get needHomeDir =>\n      'Se você é usuário de Synology, [veja aqui](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Usuários de outros sistemas precisam pesquisar como criar um diretório home.';\n\n  @override\n  String get needRestart => 'Necessita reiniciar o app';\n\n  @override\n  String get netViewType => 'Tipo de visualização de rede';\n\n  @override\n  String get newContainer => 'Novo contêiner';\n\n  @override\n  String get noConnectionStatsData => 'Não há dados de estatísticas de conexão';\n\n  @override\n  String get noLineChart => 'Não usar gráficos de linha';\n\n  @override\n  String get noLineChartForCpu => 'Não utilizar gráficos de linhas para a CPU';\n\n  @override\n  String get noPrivateKeyTip =>\n      'A chave privada não existe, pode ter sido deletada ou há um erro de configuração.';\n\n  @override\n  String get noPromptAgain => 'Não perguntar novamente';\n\n  @override\n  String get onServerDetailPage => 'Na página de detalhes do servidor';\n\n  @override\n  String get onlyOneLine => 'Exibir apenas como uma linha (rolável)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 =>\n      'Efetivo apenas quando o número de núcleos > 8';\n\n  @override\n  String get openLastPath => 'Abrir o último caminho';\n\n  @override\n  String get openLastPathTip =>\n      'Registros diferentes para servidores diferentes, e registra o caminho ao sair';\n\n  @override\n  String get parseContainerStatsTip =>\n      'Análise de status do Docker pode ser lenta';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$percent% de $size';\n  }\n\n  @override\n  String get permission => 'Permissões';\n\n  @override\n  String get pingInputIP => 'Por favor, insira o IP ou domínio alvo';\n\n  @override\n  String get pingNoServer =>\n      'Nenhum servidor disponível para Ping\\nPor favor, adicione um servidor na aba de servidores e tente novamente';\n\n  @override\n  String get plugInType => 'Tipo de Inserção';\n\n  @override\n  String get preferDiskAmount => 'Priorizar a exibição da capacidade do disco';\n\n  @override\n  String get privateKey => 'Chave privada';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'Chave privada [$keyId] não encontrada.';\n  }\n\n  @override\n  String get pushToken => 'Token de notificação push';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'Não recomendado para ativar, cuidado com os riscos de segurança! Se estiver usando o certificado padrão do PVE, você precisa habilitar esta opção.';\n\n  @override\n  String get pveLoginFailed =>\n      'Falha no login. Não é possível autenticar com o nome de usuário/senha da configuração do servidor para login no Linux PAM.';\n\n  @override\n  String get pveVersionLow =>\n      'Esta funcionalidade está atualmente em fase de teste e foi testada apenas no PVE 8+. Por favor, use com cautela.';\n\n  @override\n  String get read => 'Leitura';\n\n  @override\n  String get recentConnections => 'Conexões recentes';\n\n  @override\n  String get rememberPwdInMem => 'Lembrar senha na memória';\n\n  @override\n  String get rememberPwdInMemTip => 'Usado para contêineres, suspensão, etc.';\n\n  @override\n  String get rememberWindowSize => 'Lembrar o tamanho da janela';\n\n  @override\n  String get remotePath => 'Caminho remoto';\n\n  @override\n  String get result => 'Resultado';\n\n  @override\n  String get rotateAngel => 'Ângulo de rotação';\n\n  @override\n  String get sameIdServerExist => 'Já existe um servidor com o mesmo ID';\n\n  @override\n  String get save => 'Salvar';\n\n  @override\n  String get second => 'Segundo';\n\n  @override\n  String get serverDetailOrder =>\n      'Ordem dos componentes na página de detalhes do servidor';\n\n  @override\n  String get serverFuncBtns => 'Botões de função do servidor';\n\n  @override\n  String get serverOrder => 'Ordem do servidor';\n\n  @override\n  String get serverTabRequired => 'A aba do servidor não pode ser removida';\n\n  @override\n  String get sftpDlPrepare => 'Preparando para conectar ao servidor...';\n\n  @override\n  String get sftpEditorTip =>\n      'Se vazio, use o editor de arquivos integrado do aplicativo. Se houver um valor, use o editor do servidor remoto, por exemplo, `vim` (recomendado detectar automaticamente de acordo com `EDITOR`).';\n\n  @override\n  String get sftpRmrDirSummary => 'Usar `rm -r` em SFTP para excluir pastas';\n\n  @override\n  String get sftpSSHConnected => 'SFTP conectado...';\n\n  @override\n  String get sftpShowFoldersFirst => 'Mostrar pastas primeiro';\n\n  @override\n  String get showDistLogo => 'Mostrar logo da distribuição';\n\n  @override\n  String get size => 'Tamanho';\n\n  @override\n  String get softWrap => 'Quebra de linha suave';\n\n  @override\n  String get specifyDev => 'Especificar dispositivo';\n\n  @override\n  String get specifyDevTip =>\n      'Por exemplo, as estatísticas de tráfego de rede são por padrão para todos os dispositivos. Você pode especificar um dispositivo específico aqui.';\n\n  @override\n  String get speed => 'Velocidade';\n\n  @override\n  String spentTime(Object time) {\n    return 'Tempo gasto: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'Todos os servidores já existem (encontradas $duplicateCount duplicatas)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount duplicatas serão ignoradas';\n  }\n\n  @override\n  String get sshConfigFound => 'Encontramos configuração SSH no seu sistema';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return 'Encontrados $totalCount servidores';\n  }\n\n  @override\n  String get sshConfigImport => 'Importar Configuração SSH';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Só é possível importar informações básicas, por exemplo: IP/Porta.';\n\n  @override\n  String get sshConfigImportPermission =>\n      'Gostaria de dar permissão para ler ~/.ssh/config e importar automaticamente as configurações do servidor?';\n\n  @override\n  String get sshConfigImportTip =>\n      'Sugestão para ler ~/.ssh/config na criação do primeiro servidor';\n\n  @override\n  String sshConfigImported(Object count) {\n    return 'Importados $count servidores da configuração SSH';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return 'A chave de host SSH de $serverName foi alterada. Continue apenas se confiar neste servidor.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Impressão digital (MD5 Base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Impressão digital (MD5 hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'Tipo de chave de host SSH';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return 'Uma nova chave de host SSH foi recebida de $serverName. Verifique a impressão digital antes de confiar.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Impressão digital armazenada: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      'Gostaria de selecionar manualmente o arquivo de configuração SSH?';\n\n  @override\n  String get sshConfigNoServers =>\n      'Nenhum servidor encontrado na configuração SSH';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'Não é possível acessar o arquivo de configuração SSH devido às permissões do macOS.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount servidores serão importados';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'Quando o terminal é rolável, arrastar horizontalmente pode selecionar texto. Clicar no botão do teclado ativa/desativa o teclado. O ícone de arquivo abre o SFTP do caminho atual. O botão da área de transferência copia o conteúdo quando o texto é selecionado e cola o conteúdo da área de transferência no terminal quando nenhum texto é selecionado e há conteúdo na área de transferência. O ícone de código cola trechos de código no terminal e os executa.';\n\n  @override\n  String sshTip(Object url) {\n    return 'Esta funcionalidade está em fase de teste.\\n\\nPor favor, reporte problemas em $url ou junte-se a nós no desenvolvimento.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff =>\n      'Desativação automática das teclas virtuais';\n\n  @override\n  String get stat => 'Estatísticas';\n\n  @override\n  String get supportFmtArgs => 'Suporta os seguintes argumentos formatados:';\n\n  @override\n  String get suspendTip =>\n      'A função de suspensão requer permissões de root e suporte do systemd.';\n\n  @override\n  String switchTo(Object val) {\n    return 'Mudar para $val';\n  }\n\n  @override\n  String get syncTip =>\n      'Pode ser necessário reiniciar para algumas mudanças surtirem efeito.';\n\n  @override\n  String get system => 'Sistema';\n\n  @override\n  String get tag => 'Tag';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Toque no botão de pesquisa para descobrir servidores SSH na sua rede';\n\n  @override\n  String get termFontSizeTip =>\n      'Esta configuração afetará o tamanho do terminal (largura e altura). Você pode dar zoom na página do terminal para ajustar o tamanho da fonte da sessão atual.';\n\n  @override\n  String get textScaler => 'Escala de texto';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (tamanho original), afeta apenas algumas fontes na página do servidor, não é recomendado alterar.';\n\n  @override\n  String get time => 'Tempo';\n\n  @override\n  String get times => 'Vezes';\n\n  @override\n  String get trySudo => 'Tentar usar sudo';\n\n  @override\n  String get unknown => 'Desconhecido';\n\n  @override\n  String get unkownConvertMode => 'Modo de conversão desconhecido';\n\n  @override\n  String get update => 'Atualizar';\n\n  @override\n  String get updateIntervalEqual0 =>\n      'Se definido como 0, o estado do servidor não será atualizado automaticamente.\\nE o uso da CPU não poderá ser calculado.';\n\n  @override\n  String get updateServerStatusInterval =>\n      'Intervalo de atualização do estado do servidor';\n\n  @override\n  String get upsideDown => 'Inverter verticalmente';\n\n  @override\n  String get useCdn => 'Utilizando CDN';\n\n  @override\n  String get useCdnTip =>\n      'Recomenda-se que usuários não chineses usem CDN. Gostaria de usá-lo?';\n\n  @override\n  String get useNoPwd => 'Será usado sem senha';\n\n  @override\n  String get usePodmanByDefault => 'Usar Podman por padrão';\n\n  @override\n  String get used => 'Usado';\n\n  @override\n  String get view => 'Visualização';\n\n  @override\n  String get viewDetails => 'Ver detalhes';\n\n  @override\n  String get viewErr => 'Ver erro';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'Se houver texto selecionado no terminal, copia para a área de transferência, caso contrário, cola o conteúdo da área de transferência no terminal.';\n\n  @override\n  String get virtKeyHelpIME => 'Ligar/desligar o teclado';\n\n  @override\n  String get virtKeyHelpSFTP => 'Abre o caminho atual em SFTP.';\n\n  @override\n  String get waitConnection => 'Por favor, aguarde a conexão ser estabelecida';\n\n  @override\n  String get wakeLock => 'Manter acordado';\n\n  @override\n  String get watchNotPaired => 'Não há Apple Watch pareado';\n\n  @override\n  String get webdavSettingEmpty => 'Configurações de Webdav estão vazias';\n\n  @override\n  String get whenOpenApp => 'Ao abrir o app';\n\n  @override\n  String get wolTip =>\n      'Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.';\n\n  @override\n  String get write => 'Escrita';\n\n  @override\n  String get writeScriptFailTip =>\n      'Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.';\n\n  @override\n  String get writeScriptTip =>\n      'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \\n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Emulação Podman Docker detectada. Por favor, alterne para Podman nas configurações.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_ru.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Russian (`ru`).\nclass AppLocalizationsRu extends AppLocalizations {\n  AppLocalizationsRu([String locale = 'ru']) : super(locale);\n\n  @override\n  String get aboutThanks => 'Благодарности всем участникам.';\n\n  @override\n  String get acceptBeta => 'Принять обновления тестовой версии';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'В данный момент приватные ключи отсутствуют. Добавить системный приватный ключ (~/.ssh/id_rsa)?';\n\n  @override\n  String get added2List => 'Добавлено в список задач';\n\n  @override\n  String get addr => 'Адрес';\n\n  @override\n  String get alreadyLastDir => 'Уже в корневом каталоге';\n\n  @override\n  String get askAi => 'Спросить ИИ';\n\n  @override\n  String get askAiApiKey => 'Ключ API';\n\n  @override\n  String get askAiAwaitingResponse => 'Ожидание ответа ИИ...';\n\n  @override\n  String get askAiBaseUrl => 'Базовый URL';\n\n  @override\n  String get askAiCommandInserted => 'Команда вставлена в терминал';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Настройте $fields в настройках.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Подтвердите перед выполнением';\n\n  @override\n  String get askAiConversation => 'Разговор с ИИ';\n\n  @override\n  String get askAiDisclaimer =>\n      'ИИ может ошибаться. Используйте с осторожностью.';\n\n  @override\n  String get askAiFollowUpHint => 'Задайте дополнительный вопрос...';\n\n  @override\n  String get askAiInsertTerminal => 'Вставить в терминал';\n\n  @override\n  String get askAiNoResponse => 'Нет ответа';\n\n  @override\n  String get askAiRecommendedCommand => 'Команда, предложенная ИИ';\n\n  @override\n  String get askAiSelectedContent => 'Выбранное содержимое';\n\n  @override\n  String get askAiUsageHint => 'Используется в SSH-терминале';\n\n  @override\n  String get atLeastOneTab => 'Должна быть выбрана хотя бы одна вкладка';\n\n  @override\n  String get authFailTip =>\n      'Аутентификация не удалась, пожалуйста, проверьте, правильны ли пароль/ключ/хост/пользователь и т.д.';\n\n  @override\n  String get autoBackupConflict =>\n      'Может быть включено только одно автоматическое резервное копирование';\n\n  @override\n  String get autoConnect => 'Автоматическое подключение';\n\n  @override\n  String get autoRun => 'Автозапуск';\n\n  @override\n  String get autoUpdateHomeWidget =>\n      'Автоматическое обновление виджета на главном экране';\n\n  @override\n  String get availableTabs => 'Доступные вкладки';\n\n  @override\n  String get backupEncrypted => 'Резервная копия зашифрована';\n\n  @override\n  String get backupNotEncrypted => 'Резервная копия не зашифрована';\n\n  @override\n  String get backupPassword => 'Пароль резервной копии';\n\n  @override\n  String get backupPasswordRemoved => 'Пароль резервной копии удален';\n\n  @override\n  String get backupPasswordSet => 'Пароль резервной копии установлен';\n\n  @override\n  String get backupPasswordTip =>\n      'Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.';\n\n  @override\n  String get backupPasswordWrong => 'Неверный пароль резервной копии';\n\n  @override\n  String get backupTip =>\n      'Экспортированные данные могут быть зашифрованы паролем. \\nПожалуйста, храните их в безопасности.';\n\n  @override\n  String get backupVersionNotMatch =>\n      'Версия резервной копии не совпадает, восстановление невозможно';\n\n  @override\n  String get bgRun => 'Работа в фоновом режиме';\n\n  @override\n  String get bgRunTip =>\n      'Этот переключатель означает, что программа будет пытаться работать в фоновом режиме, но фактическое выполнение зависит от того, включено ли разрешение. Для нативного Android отключите «Оптимизацию батареи» для этого приложения, для MIUI измените контроль активности на «Нет ограничений».';\n\n  @override\n  String get clearAllStatsContent =>\n      'Вы уверены, что хотите очистить всю статистику соединений сервера? Это действие не может быть отменено.';\n\n  @override\n  String get clearAllStatsTitle => 'Очистить всю статистику';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return 'Вы уверены, что хотите очистить статистику соединений для сервера \\\"$serverName\\\"? Это действие не может быть отменено.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return 'Очистить статистику $serverName';\n  }\n\n  @override\n  String get clearThisServerStats => 'Очистить статистику этого сервера';\n\n  @override\n  String get compactDatabase => 'Сжать базу данных';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Размер базы данных: $size\\n\\nЭто перестроит базу данных для уменьшения размера файла. Данные не будут удалены.';\n  }\n\n  @override\n  String get closeAfterSave => 'Сохранить и закрыть';\n\n  @override\n  String get collapseUITip => 'Свернуть длинные списки в UI по умолчанию';\n\n  @override\n  String get connectionDetails => 'Детали соединения';\n\n  @override\n  String get connectionStats => 'Статистика соединений';\n\n  @override\n  String get connectionStatsDesc =>\n      'Просмотр коэффициента успешности подключения к серверу и истории';\n\n  @override\n  String get containerTrySudoTip =>\n      'Например: если пользователь в приложении установлен как aaa, но Docker установлен под пользователем root, тогда нужно включить эту опцию';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Для доступа к Docker требуется пароль sudo. Пожалуйста, введите ваш пароль.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'Пароль sudo неверен или не разрешён. Пожалуйста, попробуйте снова.';\n\n  @override\n  String get convert => 'Конвертировать';\n\n  @override\n  String get copyPath => 'Копировать путь';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Отобразите уровень использования каждого процессора в виде индикатора выполнения (старый стиль)';\n\n  @override\n  String get cursorType => 'Тип курсора';\n\n  @override\n  String get customCmd => 'Пользовательские команды';\n\n  @override\n  String get customCmdHint => '\\\"Имя команды\\\": \\\"Команда\\\"';\n\n  @override\n  String get deleteServers => 'Удалить серверы пакетно';\n\n  @override\n  String get desktopTerminalTip =>\n      'Команда для открытия эмулятора терминала при запуске SSH-сеансов.';\n\n  @override\n  String get dirEmpty => 'Пожалуйста, убедитесь, что папка пуста';\n\n  @override\n  String get discoverSshServers => 'Обнаружить SSH серверы';\n\n  @override\n  String get discoveryFailed => 'Обнаружение не удалось';\n\n  @override\n  String get discoverySettings => 'Настройки обнаружения';\n\n  @override\n  String get discoverySummary => 'Сводка обнаружения';\n\n  @override\n  String get diskHealth => 'Состояние диска';\n\n  @override\n  String get diskIgnorePath => 'Игнорировать путь к диску';\n\n  @override\n  String get displayCpuIndex => 'Отобразить индекс ЦП';\n\n  @override\n  String dl2Local(Object fileName) {\n    return 'Загрузить $fileName на локальный диск?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'Нет запущенных контейнеров.\\nЭто может быть из-за:\\n- пользователя Docker, отличного от пользователя, настроенного в приложении\\n- переменной окружения DOCKER_HOST, которая не была правильно считана. Вы можете выполнить `echo \\$DOCKER_HOST` в терминале, чтобы увидеть ее значение.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return 'Всего $count образов';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker не установлен';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount запущено, $stoppedCount остановлено';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count контейнеров запущено';\n  }\n\n  @override\n  String get doubleColumnMode => 'Режим двойной колонки';\n\n  @override\n  String get doubleColumnTip =>\n      'Эта опция лишь включает функцию; фактическое применение зависит от ширины устройства';\n\n  @override\n  String get editVirtKeys => 'Редактировать виртуальные клавиши';\n\n  @override\n  String get editorHighlightTip =>\n      'Текущая производительность подсветки кода неудовлетворительна, можно отключить для улучшения.';\n\n  @override\n  String get enableMdns => 'Включить mDNS';\n\n  @override\n  String get enableMdnsDesc =>\n      'Использовать mDNS/Bonjour для обнаружения SSH служб';\n\n  @override\n  String get envVars => 'Переменная окружения';\n\n  @override\n  String get experimentalFeature => 'Экспериментальная функция';\n\n  @override\n  String get extraArgs => 'Дополнительные аргументы';\n\n  @override\n  String get fallbackSshDest => 'Резервное место назначения SSH';\n\n  @override\n  String get fdroidReleaseTip =>\n      'Если вы скачали это приложение с F-Droid, рекомендуется отключить эту опцию.';\n\n  @override\n  String get fgService => 'Сервис переднего плана';\n\n  @override\n  String get fgServiceTip =>\n      'После включения некоторые модели устройств могут вылетать. Отключение может привести к тому, что некоторые модели не смогут поддерживать SSH-соединения в фоновом режиме. Пожалуйста, разрешите ServerBox права на уведомления, фоновую работу и самопробуждение в системных настройках.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'Файл \\'$file\\' слишком большой \\'$size\\', превышает $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Завершено в';\n\n  @override\n  String get followSystem => 'Следовать за системой';\n\n  @override\n  String get fontSize => 'Размер шрифта';\n\n  @override\n  String get fullScreen => 'Полноэкранный режим';\n\n  @override\n  String get fullScreenJitter => 'Вибрация в полноэкранном режиме';\n\n  @override\n  String get fullScreenJitterHelp => 'Предотвращение выгорания экрана';\n\n  @override\n  String get fullScreenTip =>\n      'Следует ли включить полноэкранный режим, когда устройство поворачивается в альбомный режим? Эта опция применяется только к вкладке сервера.';\n\n  @override\n  String get goBackQ => 'Вернуться?';\n\n  @override\n  String get goto => 'Перейти к';\n\n  @override\n  String get hideTitleBar => 'Скрыть заголовок';\n\n  @override\n  String get highlight => 'Подсветка кода';\n\n  @override\n  String get homeTabs => 'Вкладки дома';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Настройте, какие вкладки появляются на главной странице и их порядок';\n\n  @override\n  String get homeWidgetUrlConfig => 'Конфигурация URL виджета домашнего экрана';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'ошибка запроса, код: $code';\n  }\n\n  @override\n  String get ignoreCert => 'Игнорировать сертификат';\n\n  @override\n  String get image => 'Образ';\n\n  @override\n  String get imagesList => 'Список образов';\n\n  @override\n  String get installDockerWithUrl =>\n      'Пожалуйста, сначала установите Docker по адресу https://docs.docker.com/engine/install';\n\n  @override\n  String get invalid => 'Недействительный';\n\n  @override\n  String get invalidHostFormat =>\n      'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.';\n\n  @override\n  String get jumpServer => 'прыжковый сервер';\n\n  @override\n  String get keepForeground => 'Пожалуйста, держите приложение в фокусе!';\n\n  @override\n  String get keepStatusWhenErr => 'Сохранять статус сервера при ошибке';\n\n  @override\n  String get keepStatusWhenErrTip =>\n      'Применимо только в случае ошибки выполнения скрипта';\n\n  @override\n  String get keyAuth => 'Аутентификация по ключу';\n\n  @override\n  String get lastFailure => 'Последний сбой';\n\n  @override\n  String get lastSuccess => 'Последний успех';\n\n  @override\n  String get letterCache => 'Кэширование букв';\n\n  @override\n  String get letterCacheTip =>\n      'Рекомендуется отключить, но после отключения будет невозможно вводить символы CJK.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return 'Создано с ❤️ by $myGithub';\n  }\n\n  @override\n  String get max => 'максимум';\n\n  @override\n  String get maxConcurrency => 'Максимальная параллельность';\n\n  @override\n  String get maxRetryCount =>\n      'Максимальное количество попыток переподключения к серверу';\n\n  @override\n  String get maxRetryCountEqual0 =>\n      'Будет бесконечно пытаться переподключиться';\n\n  @override\n  String get min => 'минимум';\n\n  @override\n  String get more => 'Больше';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'Включено: кнопки функций сервера отображаются под каждой карточкой на вкладке сервера. Выключено: отображается в верхней части страницы деталей сервера.';\n\n  @override\n  String get needHomeDir =>\n      'Если вы пользователь Synology, [смотрите здесь](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Пользователям других систем нужно искать, как создать домашний каталог.';\n\n  @override\n  String get needRestart => 'Требуется перезапуск приложения';\n\n  @override\n  String get netViewType => 'Тип визуализации сети';\n\n  @override\n  String get newContainer => 'Создать контейнер';\n\n  @override\n  String get noConnectionStatsData => 'Нет данных статистики соединений';\n\n  @override\n  String get noLineChart => 'Не использовать линейные графики';\n\n  @override\n  String get noLineChartForCpu => 'Не используйте линейные графики для ЦП';\n\n  @override\n  String get noPrivateKeyTip =>\n      'Приватный ключ не существует, возможно, он был удален или есть ошибка в настройках.';\n\n  @override\n  String get noPromptAgain => 'Больше не спрашивать';\n\n  @override\n  String get onServerDetailPage => 'На странице деталей сервера';\n\n  @override\n  String get onlyOneLine => 'Отображать только в одной строке (прокручивается)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 =>\n      'Действует только при количестве ядер больше 8';\n\n  @override\n  String get openLastPath => 'Открыть последний путь';\n\n  @override\n  String get openLastPathTip =>\n      'Для разных серверов будут сохранены разные записи, записывается путь при выходе';\n\n  @override\n  String get parseContainerStatsTip =>\n      'Анализ статуса использования Docker может быть медленным';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$percent% от $size';\n  }\n\n  @override\n  String get permission => 'Разрешения';\n\n  @override\n  String get pingInputIP => 'Пожалуйста, введите целевой IP или домен';\n\n  @override\n  String get pingNoServer =>\n      'Нет доступных серверов для Ping\\nПожалуйста, добавьте их на вкладке «Сервер» и попробуйте снова';\n\n  @override\n  String get plugInType => 'Тип вставки';\n\n  @override\n  String get preferDiskAmount => 'Приоритетное отображение объёма диска';\n\n  @override\n  String get privateKey => 'Приватный ключ';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'Закрытый ключ [$keyId] не найден.';\n  }\n\n  @override\n  String get pushToken => 'Токен уведомлений';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'Не рекомендуется включать, обратите внимание на риски безопасности! Если вы используете стандартный сертификат от PVE, вам нужно включить эту опцию.';\n\n  @override\n  String get pveLoginFailed =>\n      'Ошибка входа. Невозможно аутентифицироваться с помощью имени пользователя/пароля из конфигурации сервера для входа в Linux PAM.';\n\n  @override\n  String get pveVersionLow =>\n      'Эта функция в настоящее время находится на стадии тестирования и была протестирована только на PVE 8+. Используйте ее с осторожностью.';\n\n  @override\n  String get read => 'Чтение';\n\n  @override\n  String get recentConnections => 'Недавние соединения';\n\n  @override\n  String get rememberPwdInMem => 'Запомнить пароль в памяти';\n\n  @override\n  String get rememberPwdInMemTip =>\n      'Используется для контейнеров, приостановки и т. д.';\n\n  @override\n  String get rememberWindowSize => 'Запомнить размер окна';\n\n  @override\n  String get remotePath => 'Удаленный путь';\n\n  @override\n  String get result => 'Результат';\n\n  @override\n  String get rotateAngel => 'Угол поворота';\n\n  @override\n  String get sameIdServerExist => 'Сервер с таким ID уже существует';\n\n  @override\n  String get save => 'Сохранить';\n\n  @override\n  String get second => 'с';\n\n  @override\n  String get serverDetailOrder =>\n      'Порядок элементов на странице деталей сервера';\n\n  @override\n  String get serverFuncBtns => 'Кнопки функций сервера';\n\n  @override\n  String get serverOrder => 'Порядок серверов';\n\n  @override\n  String get serverTabRequired => 'Вкладку сервера нельзя удалить';\n\n  @override\n  String get sftpDlPrepare => 'Подготовка подключения...';\n\n  @override\n  String get sftpEditorTip =>\n      'Если пусто, используйте встроенный редактор файлов приложения. Если значение указано, используйте редактор удаленного сервера, например, `vim` (рекомендуется автоматически определять согласно `EDITOR`).';\n\n  @override\n  String get sftpRmrDirSummary =>\n      'Использовать `rm -r` в SFTP для удаления папок';\n\n  @override\n  String get sftpSSHConnected => 'SFTP подключен...';\n\n  @override\n  String get sftpShowFoldersFirst => 'Показывать папки в начале';\n\n  @override\n  String get showDistLogo => 'Показать лого дистрибутива';\n\n  @override\n  String get size => 'Размер';\n\n  @override\n  String get softWrap => 'Мягкий перенос';\n\n  @override\n  String get specifyDev => 'Указать устройство';\n\n  @override\n  String get specifyDevTip =>\n      'Например, статистика сетевого трафика по умолчанию относится ко всем устройствам. Здесь вы можете указать конкретное устройство.';\n\n  @override\n  String get speed => 'Скорость';\n\n  @override\n  String spentTime(Object time) {\n    return 'Затрачено времени: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'Все серверы уже существуют (найдено $duplicateCount дубликатов)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount дубликатов будут пропущены';\n  }\n\n  @override\n  String get sshConfigFound => 'Мы нашли SSH-конфигурацию в вашей системе';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return 'Найдено $totalCount серверов';\n  }\n\n  @override\n  String get sshConfigImport => 'Импорт SSH Конфигурации';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Можно импортировать только базовую информацию, например: IP/порт.';\n\n  @override\n  String get sshConfigImportPermission =>\n      'Хотите ли вы дать разрешение на чтение ~/.ssh/config и автоматический импорт настроек сервера?';\n\n  @override\n  String get sshConfigImportTip =>\n      'Предложение прочитать ~/.ssh/config при создании первого сервера';\n\n  @override\n  String sshConfigImported(Object count) {\n    return 'Импортировано $count серверов из SSH-конфигурации';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return 'SSH-ключ хоста для $serverName изменился. Продолжайте только если доверяете этому серверу.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Отпечаток (MD5 Base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Отпечаток (MD5 hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'Тип ключа хоста SSH';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return 'Получен новый SSH-ключ хоста от $serverName. Проверьте отпечаток перед продолжением.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Сохранённый отпечаток: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      'Хотели бы вы вручную выбрать файл конфигурации SSH?';\n\n  @override\n  String get sshConfigNoServers => 'Серверы не найдены в SSH-конфигурации';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'Невозможно получить доступ к файлу конфигурации SSH из-за разрешений macOS.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount серверов будут импортированы';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.';\n\n  @override\n  String sshTip(Object url) {\n    return 'Эта функция находится в стадии тестирования.\\n\\nПожалуйста, отправляйте отчеты о проблемах на $url или присоединяйтесь к нашей разработке.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff =>\n      'Автоматическое переключение виртуальных клавиш';\n\n  @override\n  String get stat => 'Статистика';\n\n  @override\n  String get supportFmtArgs => 'Поддерживаются следующие форматы аргументов:';\n\n  @override\n  String get suspendTip =>\n      'Функция приостановки требует прав root и поддержки systemd.';\n\n  @override\n  String switchTo(Object val) {\n    return 'Переключиться на $val';\n  }\n\n  @override\n  String get syncTip =>\n      'Возможно, потребуется перезагрузка, чтобы некоторые изменения вступили в силу.';\n\n  @override\n  String get system => 'Система';\n\n  @override\n  String get tag => 'Теги';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Нажмите кнопку поиска, чтобы обнаружить SSH серверы в вашей сети';\n\n  @override\n  String get termFontSizeTip =>\n      'Эта настройка повлияет на размер терминала (ширина и высота). Вы можете масштабировать страницу терминала, чтобы调整 размер шрифта текущей сессии.';\n\n  @override\n  String get textScaler => 'Масштабирование текста';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (исходный размер), применяется только к части шрифтов на странице сервера, изменение не рекомендуется.';\n\n  @override\n  String get time => 'Время';\n\n  @override\n  String get times => 'Раз';\n\n  @override\n  String get trySudo => 'Попробовать использовать sudo';\n\n  @override\n  String get unknown => 'Неизвестно';\n\n  @override\n  String get unkownConvertMode => 'Неизвестный режим конвертации';\n\n  @override\n  String get update => 'Обновление';\n\n  @override\n  String get updateIntervalEqual0 =>\n      'Если установлено 0, статус сервера не будет автоматически обновляться.\\nТакже не будет рассчитано использование ЦП.';\n\n  @override\n  String get updateServerStatusInterval =>\n      'Интервал обновления статуса сервера';\n\n  @override\n  String get upsideDown => 'Перевернуть';\n\n  @override\n  String get useCdn => 'Использование CDN';\n\n  @override\n  String get useCdnTip =>\n      'Не китайским пользователям рекомендуется использовать CDN. Хотели бы вы его использовать?';\n\n  @override\n  String get useNoPwd => 'Будет использоваться без пароля';\n\n  @override\n  String get usePodmanByDefault => 'Использовать Podman по умолчанию';\n\n  @override\n  String get used => 'Использовано';\n\n  @override\n  String get view => 'Вид';\n\n  @override\n  String get viewDetails => 'Просмотр деталей';\n\n  @override\n  String get viewErr => 'Просмотр ошибок';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'Если в терминале выделен текст, то он копируется в буфер обмена, в противном случае содержимое буфера вставляется в терминал.';\n\n  @override\n  String get virtKeyHelpIME => 'Включить/выключить клавиатуру';\n\n  @override\n  String get virtKeyHelpSFTP => 'Открыть текущий путь в SFTP.';\n\n  @override\n  String get waitConnection => 'Пожалуйста, дождитесь установки соединения';\n\n  @override\n  String get wakeLock => 'Держать включенным';\n\n  @override\n  String get watchNotPaired => 'Apple Watch не сопряжены';\n\n  @override\n  String get webdavSettingEmpty => 'Настройки Webdav пусты';\n\n  @override\n  String get whenOpenApp => 'При открытии приложения';\n\n  @override\n  String get wolTip =>\n      'После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.';\n\n  @override\n  String get write => 'Запись';\n\n  @override\n  String get writeScriptFailTip =>\n      'Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.';\n\n  @override\n  String get writeScriptTip =>\n      'После подключения к серверу скрипт будет записан в `~/.config/server_box` \\n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Обнаружена эмуляция Podman Docker. Пожалуйста, переключитесь на Podman в настройках.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_tr.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Turkish (`tr`).\nclass AppLocalizationsTr extends AppLocalizations {\n  AppLocalizationsTr([String locale = 'tr']) : super(locale);\n\n  @override\n  String get aboutThanks => 'Aşağıdaki katılımcılara teşekkürler.';\n\n  @override\n  String get acceptBeta => 'Beta sürüm güncellemelerini kabul et';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'Şu anda özel anahtarlar mevcut değil, sistemle birlikte gelen anahtarı (~/.ssh/id_rsa) eklemek ister misiniz?';\n\n  @override\n  String get added2List => 'Görev listesine eklendi';\n\n  @override\n  String get addr => 'Adres';\n\n  @override\n  String get alreadyLastDir => 'Zaten son dizindesiniz.';\n\n  @override\n  String get askAi => 'Yapay zekaya sor';\n\n  @override\n  String get askAiApiKey => 'API anahtarı';\n\n  @override\n  String get askAiAwaitingResponse => 'Yapay zekâ yanıtı bekleniyor...';\n\n  @override\n  String get askAiBaseUrl => 'Temel URL';\n\n  @override\n  String get askAiCommandInserted => 'Komut terminale eklendi';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Lütfen Ayarlar\\'da $fields öğesini yapılandırın.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Çalıştırmadan önce onayla';\n\n  @override\n  String get askAiConversation => 'YZ sohbeti';\n\n  @override\n  String get askAiDisclaimer =>\n      'Yapay zeka hata yapabilir. Lütfen dikkatli kullanın.';\n\n  @override\n  String get askAiFollowUpHint => 'Yeni bir soru sor...';\n\n  @override\n  String get askAiInsertTerminal => 'Terminale ekle';\n\n  @override\n  String get askAiNoResponse => 'Yanıt yok';\n\n  @override\n  String get askAiRecommendedCommand => 'YZ önerilen komut';\n\n  @override\n  String get askAiSelectedContent => 'Seçilen içerik';\n\n  @override\n  String get askAiUsageHint => 'SSH Terminalinde kullanılır';\n\n  @override\n  String get atLeastOneTab => 'En az bir sekme seçilmelidir';\n\n  @override\n  String get authFailTip =>\n      'Kimlik doğrulama başarısız oldu, lütfen kimlik bilgilerinin doğru olup olmadığını kontrol edin';\n\n  @override\n  String get autoBackupConflict =>\n      'Aynı anda yalnızca bir otomatik yedekleme açık olabilir.';\n\n  @override\n  String get autoConnect => 'Otomatik bağlan';\n\n  @override\n  String get autoRun => 'Otomatik çalıştır';\n\n  @override\n  String get autoUpdateHomeWidget => 'Ana ekran bileşenini otomatik güncelle';\n\n  @override\n  String get availableTabs => 'Mevcut Sekmeler';\n\n  @override\n  String get backupEncrypted => 'Yedekleme şifrelenmiş';\n\n  @override\n  String get backupNotEncrypted => 'Yedekleme şifreli değil';\n\n  @override\n  String get backupPassword => 'Yedekleme parolası';\n\n  @override\n  String get backupPasswordRemoved => 'Yedekleme parolası kaldırıldı';\n\n  @override\n  String get backupPasswordSet => 'Yedekleme parolası ayarlandı';\n\n  @override\n  String get backupPasswordTip =>\n      'Yedekleme dosyalarını şifrelemek için bir parola belirleyin. Şifrelemeyi devre dışı bırakmak için boş bırakın.';\n\n  @override\n  String get backupPasswordWrong => 'Yanlış yedekleme parolası';\n\n  @override\n  String get backupTip =>\n      'Dışa aktarılan veriler parola ile şifrelenebilir. \\nLütfen güvenli bir şekilde saklayın.';\n\n  @override\n  String get backupVersionNotMatch => 'Yedekleme sürümü eşleşmiyor.';\n\n  @override\n  String get bgRun => 'Arka planda çalıştır';\n\n  @override\n  String get bgRunTip =>\n      'Bu anahtar yalnızca programın arka planda çalışmayı deneyeceği anlamına gelir. Arka planda çalışıp çalışamayacağı, iznin etkinleştirilip etkinleştirilmediğine bağlıdır. AOSP tabanlı Android ROM\\'lar için lütfen bu uygulamada \\\"Pil Optimizasyonu\\\"nu devre dışı bırakın. MIUI / HyperOS için lütfen güç tasarrufu politikasını \\\"Sınırsız\\\" olarak değiştirin.';\n\n  @override\n  String get clearAllStatsContent =>\n      'Tüm sunucu bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.';\n\n  @override\n  String get clearAllStatsTitle => 'Tüm İstatistikleri Temizle';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return '\\\"$serverName\\\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return '$serverName İstatistiklerini Temizle';\n  }\n\n  @override\n  String get clearThisServerStats => 'Bu Sunucu İstatistiklerini Temizle';\n\n  @override\n  String get compactDatabase => 'Veritabanını Sıkıştır';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Veritabanı boyutu: $size\\n\\nBu, dosya boyutunu küçültmek için veritabanını yeniden düzenleyecektir. Veriler silinmeyecek.';\n  }\n\n  @override\n  String get closeAfterSave => 'Kaydet ve kapat';\n\n  @override\n  String get collapseUITip =>\n      'Arayüzde uzun listelerin varsayılan olarak daraltılıp daraltılmayacağı';\n\n  @override\n  String get connectionDetails => 'Bağlantı Detayları';\n\n  @override\n  String get connectionStats => 'Bağlantı İstatistikleri';\n\n  @override\n  String get connectionStatsDesc =>\n      'Sunucu bağlantı başarı oranını ve geçmişi görüntüle';\n\n  @override\n  String get containerTrySudoTip =>\n      'Örneğin: Uygulamada kullanıcı aaa olarak ayarlanmış, ancak Docker root kullanıcısı altında kurulmuş. Bu durumda bu seçeneği etkinleştirmeniz gerekir.';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Docker\\'e erişmek için sudo şifresi gereklidir. Lütfen şifrenizi girin.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'Sudo şifresi yanlış veya izin verilmiyor. Lütfen tekrar deneyin.';\n\n  @override\n  String get convert => 'Dönüştür';\n\n  @override\n  String get copyPath => 'Yolu kopyala';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Her CPU\\'nun kullanımını ilerleme çubuğu tarzında göster (eski tarz)';\n\n  @override\n  String get cursorType => 'İmleç türü';\n\n  @override\n  String get customCmd => 'Özel komutlar';\n\n  @override\n  String get customCmdHint => '\\\"Komut Adı\\\": \\\"Komut\\\"';\n\n  @override\n  String get deleteServers => 'Sunucuları toplu sil';\n\n  @override\n  String get desktopTerminalTip =>\n      'SSH oturumları başlatılırken terminal öykünücüsünü açmak için kullanılan komut.';\n\n  @override\n  String get dirEmpty => 'Klasörün boş olduğundan emin olun.';\n\n  @override\n  String get discoverSshServers => 'SSH Sunucularını Keşfet';\n\n  @override\n  String get discoveryFailed => 'Keşif başarısız';\n\n  @override\n  String get discoverySettings => 'Keşif Ayarları';\n\n  @override\n  String get discoverySummary => 'Keşif Özeti';\n\n  @override\n  String get diskHealth => 'Disk sağlığı';\n\n  @override\n  String get diskIgnorePath => 'Disk için yok sayılan yol';\n\n  @override\n  String get displayCpuIndex => 'CPU indeksini göster';\n\n  @override\n  String dl2Local(Object fileName) {\n    return '$fileName dosyasını yerel cihaza indir?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'Çalışan konteyner yok.\\nBunun nedeni şunlar olabilir:\\n- Docker kurulum kullanıcısı, uygulamada yapılandırılan kullanıcı adıyla aynı değil.\\n- DOCKER_HOST ortam değişkeni doğru okunmadı. Terminalde `echo \\$DOCKER_HOST` komutunu çalıştırarak kontrol edebilirsiniz.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '$count görüntü';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker kurulmamış';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount çalışıyor, $stoppedCount konteyner durdurulmuş.';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count konteyner çalışıyor.';\n  }\n\n  @override\n  String get doubleColumnMode => 'Çift sütun modu';\n\n  @override\n  String get doubleColumnTip =>\n      'Bu seçenek yalnızca özelliği etkinleştirir, gerçekten etkinleşip etkinleşmeyeceği cihazın genişliğine bağlıdır';\n\n  @override\n  String get editVirtKeys => 'Sanal tuşları düzenle';\n\n  @override\n  String get editorHighlightTip =>\n      'Mevcut kod vurgulama performansı ideal değil ve isteğe bağlı olarak kapatılabilir.';\n\n  @override\n  String get enableMdns => 'mDNS\\'yi Etkinleştir';\n\n  @override\n  String get enableMdnsDesc =>\n      'SSH hizmetlerini keşfetmek için mDNS/Bonjour kullan';\n\n  @override\n  String get envVars => 'Ortam değişkeni';\n\n  @override\n  String get experimentalFeature => 'Deneysel özellik';\n\n  @override\n  String get extraArgs => 'Ek argümanlar';\n\n  @override\n  String get fallbackSshDest => 'Yedek SSH hedefi';\n\n  @override\n  String get fdroidReleaseTip =>\n      'Bu uygulamayı F-Droid\\'den indirdiyseniz, bu seçeneği kapatmanız önerilir.';\n\n  @override\n  String get fgService => 'Ön Plan Servisi';\n\n  @override\n  String get fgServiceTip =>\n      'Etkinleştirildikten sonra bazı cihaz modellerinde çökme olabilir. Devre dışı bırakmak, bazı modellerde SSH bağlantılarının arka planda sürdürülememesine neden olabilir. Lütfen sistem ayarlarında ServerBox bildirim izinlerini, arka planda çalışmayı ve otomatik uyanmayı etkinleştirin.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return '\\'$file\\' dosyası çok büyük $size, maksimum $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Tamamlandı:';\n\n  @override\n  String get followSystem => 'Sistemi takip et';\n\n  @override\n  String get fontSize => 'Yazı tipi boyutu';\n\n  @override\n  String get fullScreen => 'Tam ekran modu';\n\n  @override\n  String get fullScreenJitter => 'Tam ekran titreşim';\n\n  @override\n  String get fullScreenJitterHelp => 'Ekran yanmasını önlemek için';\n\n  @override\n  String get fullScreenTip =>\n      'Cihaz yatay moda döndürüldüğünde tam ekran modu etkinleştirilsin mi? Bu seçenek yalnızca sunucu sekmesi için geçerlidir.';\n\n  @override\n  String get goBackQ => 'Geri dön?';\n\n  @override\n  String get goto => 'Git';\n\n  @override\n  String get hideTitleBar => 'Başlık çubuğunu gizle';\n\n  @override\n  String get highlight => 'Kod vurgulama';\n\n  @override\n  String get homeTabs => 'Ana Sayfa Sekmeleri';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Ana sayfada görünecek sekmeleri ve sıralarını özelleştirin';\n\n  @override\n  String get homeWidgetUrlConfig => 'Ana ekran bileşeni URL\\'sini yapılandır';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'İstek başarısız oldu, durum kodu: $code';\n  }\n\n  @override\n  String get ignoreCert => 'Sertifikayı yok say';\n\n  @override\n  String get image => 'Görüntü';\n\n  @override\n  String get imagesList => 'Görüntü listesi';\n\n  @override\n  String get installDockerWithUrl =>\n      'Lütfen önce https://docs.docker.com/engine/install adresinden Docker\\'ı kurun.';\n\n  @override\n  String get invalid => 'Geçersiz';\n\n  @override\n  String get invalidHostFormat =>\n      'Invalid host format. Only IPv4, IPv6, and domain characters are allowed.';\n\n  @override\n  String get jumpServer => 'Atlama sunucusu';\n\n  @override\n  String get keepForeground => 'Uygulamayı ön planda tut!';\n\n  @override\n  String get keepStatusWhenErr => 'Son sunucu durumunu koru';\n\n  @override\n  String get keepStatusWhenErrTip =>\n      'Yalnızca betik yürütülmesi sırasında bir hata olduğunda';\n\n  @override\n  String get keyAuth => 'Anahtar Kimlik Doğrulama';\n\n  @override\n  String get lastFailure => 'Son Başarısızlık';\n\n  @override\n  String get lastSuccess => 'Son Başarı';\n\n  @override\n  String get letterCache => 'Harf önbelleği';\n\n  @override\n  String get letterCacheTip =>\n      'Devre dışı bırakılması önerilir, ancak devre dışı bırakıldığında CJK karakterlerini girmek mümkün olmayacaktır.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return '$myGithub tarafından ❤️ ile yapıldı';\n  }\n\n  @override\n  String get max => 'maks';\n\n  @override\n  String get maxConcurrency => 'Maksimum Eşzamanlılık';\n\n  @override\n  String get maxRetryCount => 'Sunucu yeniden bağlantı sayısı';\n\n  @override\n  String get maxRetryCountEqual0 => 'Tekrar tekrar deneyecek.';\n\n  @override\n  String get min => 'min';\n\n  @override\n  String get more => 'Daha fazla';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'Açık: Sunucu Sekmesi sayfasındaki her kartın altında görüntülenebilir. Kapalı: Sunucu Ayrıntıları sayfasının üstünde görüntülenebilir.';\n\n  @override\n  String get needHomeDir =>\n      'Synology kullanıcısıysanız, [buraya bakın](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Diğer sistem kullanıcılarının bir ana dizin oluşturma yöntemini araması gerekir.';\n\n  @override\n  String get needRestart => 'Uygulamanın yeniden başlatılması gerekiyor';\n\n  @override\n  String get netViewType => 'Ağ görüntüleme türü';\n\n  @override\n  String get newContainer => 'Yeni konteyner';\n\n  @override\n  String get noConnectionStatsData => 'Bağlantı istatistik verisi yok';\n\n  @override\n  String get noLineChart => 'Çizgi grafikleri kullanma';\n\n  @override\n  String get noLineChartForCpu => 'CPU için çizgi grafikleri kullanma';\n\n  @override\n  String get noPrivateKeyTip =>\n      'Özel anahtar mevcut değil, silinmiş olabilir veya yapılandırma hatası vardır.';\n\n  @override\n  String get noPromptAgain => 'Tekrar sorma';\n\n  @override\n  String get onServerDetailPage => 'Sunucu ayrıntı sayfasında';\n\n  @override\n  String get onlyOneLine => 'Yalnızca tek satır olarak göster (kaydırılabilir)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 =>\n      'Çekirdek sayısı 8\\'den büyük olduğunda çalışır';\n\n  @override\n  String get openLastPath => 'Son yolu aç';\n\n  @override\n  String get openLastPathTip =>\n      'Farklı sunucular farklı günlükler tutar ve günlük, çıkış yoludur';\n\n  @override\n  String get parseContainerStatsTip =>\n      'Docker\\'ın doluluk durumunu ayrıştırmak oldukça yavaş.';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$size\\'ın $percent%\\'i';\n  }\n\n  @override\n  String get permission => 'İzinler';\n\n  @override\n  String get pingInputIP => 'Lütfen bir hedef IP / alan adı girin.';\n\n  @override\n  String get pingNoServer =>\n      'Ping yapılacak sunucu yok.\\nLütfen sunucu sekmesinde bir sunucu ekleyin.';\n\n  @override\n  String get plugInType => 'Eklenti Türü';\n\n  @override\n  String get preferDiskAmount => 'Disk kapasitesini öncelikli olarak göster';\n\n  @override\n  String get privateKey => 'Özel Anahtar';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'Özel anahtar [$keyId] bulunamadı.';\n  }\n\n  @override\n  String get pushToken => 'Push belirteci';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'Etkinleştirilmesi önerilmez, güvenlik risklerine dikkat edin! PVE\\'den varsayılan sertifikayı kullanıyorsanız, bu seçeneği etkinleştirmeniz gerekir.';\n\n  @override\n  String get pveLoginFailed =>\n      'Giriş başarısız. Linux PAM girişi için sunucu yapılandırmasındaki kullanıcı adı/şifre ile kimlik doğrulama yapılamadı.';\n\n  @override\n  String get pveVersionLow =>\n      'Bu özellik şu anda test aşamasında ve yalnızca PVE 8+ üzerinde test edildi. Lütfen dikkatli kullanın.';\n\n  @override\n  String get read => 'Oku';\n\n  @override\n  String get recentConnections => 'Son Bağlantılar';\n\n  @override\n  String get rememberPwdInMem => 'Şifreyi bellekte hatırla';\n\n  @override\n  String get rememberPwdInMemTip =>\n      'Konteynerler, askıya alma vb. için kullanılır.';\n\n  @override\n  String get rememberWindowSize => 'Pencere boyutunu hatırla';\n\n  @override\n  String get remotePath => 'Uzak yol';\n\n  @override\n  String get result => 'Sonuç';\n\n  @override\n  String get rotateAngel => 'Dönüş açısı';\n\n  @override\n  String get sameIdServerExist => 'Aynı kimliğe sahip bir sunucu zaten mevcut';\n\n  @override\n  String get save => 'Kaydet';\n\n  @override\n  String get second => 's';\n\n  @override\n  String get serverDetailOrder => 'Ayrıntı sayfası bileşen sırası';\n\n  @override\n  String get serverFuncBtns => 'Sunucu işlev düğmeleri';\n\n  @override\n  String get serverOrder => 'Sunucu sırası';\n\n  @override\n  String get serverTabRequired => 'Sunucu sekmesi kaldırılamaz';\n\n  @override\n  String get sftpDlPrepare => 'Bağlantı hazırlanıyor...';\n\n  @override\n  String get sftpEditorTip =>\n      'Boşsa, uygulamanın yerleşik dosya düzenleyicisi kullanılır. Bir değer varsa, uzak sunucunun düzenleyicisi kullanılır, örn. `vim` (otomatik olarak `EDITOR`\\'a göre algılanması önerilir).';\n\n  @override\n  String get sftpRmrDirSummary =>\n      'SFTP\\'de bir klasörü silmek için `rm -r` kullan.';\n\n  @override\n  String get sftpSSHConnected => 'SFTP Bağlandı';\n\n  @override\n  String get sftpShowFoldersFirst => 'Önce klasörleri göster';\n\n  @override\n  String get showDistLogo => 'Dağıtım logosunu göster';\n\n  @override\n  String get size => 'Boyut';\n\n  @override\n  String get softWrap => 'Yumuşak kaydırma';\n\n  @override\n  String get specifyDev => 'Cihazı belirt';\n\n  @override\n  String get specifyDevTip =>\n      'Örneğin, ağ trafiği istatistikleri varsayılan olarak tüm cihazlar içindir. Burada belirli bir cihaz belirtebilirsiniz.';\n\n  @override\n  String get speed => 'Hız';\n\n  @override\n  String spentTime(Object time) {\n    return 'Harcanan süre: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'Tüm sunucular zaten mevcut ($duplicateCount kopya bulundu)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount kopya atlanacak';\n  }\n\n  @override\n  String get sshConfigFound => 'Sisteminizde SSH yapılandırması bulduk';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return '$totalCount sunucu bulundu';\n  }\n\n  @override\n  String get sshConfigImport => 'SSH Yapılandırma İçe Aktarma';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Yalnızca temel bilgiler içe aktarılabilir, örneğin: IP/Port.';\n\n  @override\n  String get sshConfigImportPermission =>\n      '~/.ssh/config dosyasını okumak ve sunucu ayarlarını otomatik olarak içe aktarmak için izin vermek ister misiniz?';\n\n  @override\n  String get sshConfigImportTip =>\n      'İlk sunucu oluşturulurken ~/.ssh/config okuma istemi';\n\n  @override\n  String sshConfigImported(Object count) {\n    return 'SSH yapılandırmasından $count sunucu içe aktarıldı';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return '$serverName için SSH ana bilgisayar anahtarı değişti. Yalnızca bu sunucuya güveniyorsanız devam edin.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Parmak izi (MD5 Base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Parmak izi (MD5 hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'SSH ana bilgisayar anahtarı türü';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return '$serverName üzerinden yeni bir SSH ana bilgisayar anahtarı alındı. Güvenmeden önce parmak izini kontrol edin.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Kaydedilen parmak izi: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      'SSH yapılandırma dosyasını manuel olarak seçmek ister misiniz?';\n\n  @override\n  String get sshConfigNoServers => 'SSH yapılandırmasında sunucu bulunamadı';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'macOS izinleri nedeniyle SSH yapılandırma dosyasına erişilemiyor.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount sunucu içe aktarılacak';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'Terminal kaydırılabilir olduğunda, yatay olarak sürüklemek metni seçebilir. Klavye düğmesine tıklamak klavyeyi açar/kapar. Dosya simgesi mevcut yolu SFTP\\'de açar. Pano düğmesi, metin seçiliyken içeriği kopyalar ve metin seçili değilken panoda içerik varsa terminale yapıştırır. Kod simgesi, kod parçacıklarını terminale yapıştırır ve yürütür.';\n\n  @override\n  String sshTip(Object url) {\n    return 'Bu işlev şu anda deneysel aşamada.\\n\\nLütfen hataları $url adresinde bildirin veya geliştirmemize katılın.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff => 'Sanal tuşların otomatik geçişi';\n\n  @override\n  String get stat => 'İstatistik';\n\n  @override\n  String get supportFmtArgs =>\n      'Aşağıdaki biçimlendirme parametreleri desteklenir:';\n\n  @override\n  String get suspendTip =>\n      'Askıya alma işlevi, root izni ve systemd desteği gerektirir.';\n\n  @override\n  String switchTo(Object val) {\n    return '$val\\'a geç';\n  }\n\n  @override\n  String get syncTip =>\n      'Bazı değişikliklerin etkili olması için yeniden başlatma gerekebilir.';\n\n  @override\n  String get system => 'Sistem';\n\n  @override\n  String get tag => 'Etiketler';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Ağınızdaki SSH sunucularını keşfetmek için arama düğmesine dokunun';\n\n  @override\n  String get termFontSizeTip =>\n      'Bu ayar terminal boyutunu (genişlik ve yükseklik) etkiler. Terminal sayfasında yakınlaştırarak mevcut oturumun yazı tipi boyutunu ayarlayabilirsiniz.';\n\n  @override\n  String get textScaler => 'Metin ölçekleyici';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => %100 (orijinal boyut), yalnızca sunucu sayfasındaki yazı tipinin bir kısmı üzerinde çalışır, değiştirilmesi önerilmez.';\n\n  @override\n  String get time => 'Zaman';\n\n  @override\n  String get times => 'Kez';\n\n  @override\n  String get trySudo => 'Sudo ile dene';\n\n  @override\n  String get unknown => 'Bilinmeyen';\n\n  @override\n  String get unkownConvertMode => 'Bilinmeyen dönüşüm modu';\n\n  @override\n  String get update => 'Güncelle';\n\n  @override\n  String get updateIntervalEqual0 =>\n      '0 olarak ayarladınız, otomatik güncelleme yapılmayacak.\\nCPU durumu hesaplanamaz.';\n\n  @override\n  String get updateServerStatusInterval => 'Sunucu durumu güncelleme aralığı';\n\n  @override\n  String get upsideDown => 'Başaşağı';\n\n  @override\n  String get useCdn => 'CDN kullan';\n\n  @override\n  String get useCdnTip =>\n      'Çinli olmayan kullanıcıların CDN kullanması önerilir. Kullanmak ister misiniz?';\n\n  @override\n  String get useNoPwd => 'Şifre kullanılmayacak';\n\n  @override\n  String get usePodmanByDefault => 'Varsayılan olarak Podman kullan';\n\n  @override\n  String get used => 'Kullanılan';\n\n  @override\n  String get view => 'Görünüm';\n\n  @override\n  String get viewDetails => 'Detayları Görüntüle';\n\n  @override\n  String get viewErr => 'Hatayı gör';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'Seçili terminal boş değilse panoya kopyala, aksi takdirde panodaki içeriği terminale yapıştır.';\n\n  @override\n  String get virtKeyHelpIME => 'Klavyeyi aç/kapat';\n\n  @override\n  String get virtKeyHelpSFTP => 'Mevcut dizini SFTP\\'de aç.';\n\n  @override\n  String get waitConnection => 'Lütfen bağlantının kurulmasını bekleyin.';\n\n  @override\n  String get wakeLock => 'Uyanık tut';\n\n  @override\n  String get watchNotPaired => 'Eşleştirilmiş Apple Watch yok';\n\n  @override\n  String get webdavSettingEmpty => 'WebDav ayarı boş';\n\n  @override\n  String get whenOpenApp => 'Uygulama açıldığında';\n\n  @override\n  String get wolTip =>\n      'WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.';\n\n  @override\n  String get write => 'Yaz';\n\n  @override\n  String get writeScriptFailTip =>\n      'Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.';\n\n  @override\n  String get writeScriptTip =>\n      'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \\n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Podman Docker emülasyonu tespit edildi. Lütfen ayarlarda Podman\\'a geçin.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_uk.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.dart';\n\n// ignore_for_file: type=lint\n\n/// The translations for Ukrainian (`uk`).\nclass AppLocalizationsUk extends AppLocalizations {\n  AppLocalizationsUk([String locale = 'uk']) : super(locale);\n\n  @override\n  String get aboutThanks => 'Дякуємо наступним особам, які взяли участь.';\n\n  @override\n  String get acceptBeta => 'Прийняти оновлення бета-версії';\n\n  @override\n  String get addSystemPrivateKeyTip =>\n      'Наразі приватних ключів нема, хочете додати той, що йде з системою (~/.ssh/id_rsa)?';\n\n  @override\n  String get added2List => 'Додано до списку завдань';\n\n  @override\n  String get addr => 'Адреса';\n\n  @override\n  String get alreadyLastDir => 'Вже в останньому каталозі.';\n\n  @override\n  String get askAi => 'Запитати ШІ';\n\n  @override\n  String get askAiApiKey => 'Ключ API';\n\n  @override\n  String get askAiAwaitingResponse => 'Очікування відповіді ШІ...';\n\n  @override\n  String get askAiBaseUrl => 'Базова URL';\n\n  @override\n  String get askAiCommandInserted => 'Команду вставлено в термінал';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return 'Налаштуйте $fields у налаштуваннях.';\n  }\n\n  @override\n  String get askAiConfirmExecute => 'Підтвердити перед виконанням';\n\n  @override\n  String get askAiConversation => 'Розмова з ШІ';\n\n  @override\n  String get askAiDisclaimer => 'ШІ може помилятися. Користуйтеся обережно.';\n\n  @override\n  String get askAiFollowUpHint => 'Поставте додаткове запитання...';\n\n  @override\n  String get askAiInsertTerminal => 'Вставити в термінал';\n\n  @override\n  String get askAiNoResponse => 'Відповідь відсутня';\n\n  @override\n  String get askAiRecommendedCommand => 'Команда, запропонована ШІ';\n\n  @override\n  String get askAiSelectedContent => 'Вибраний вміст';\n\n  @override\n  String get askAiUsageHint => 'Використовується в SSH-терміналі';\n\n  @override\n  String get atLeastOneTab => 'Потрібно вибрати принаймні одну вкладку';\n\n  @override\n  String get authFailTip =>\n      'Авторизація не вдалася, будь ласка, перевірте правильність облікових даних';\n\n  @override\n  String get autoBackupConflict =>\n      'Тільки одне автоматичне резервне копіювання може бути активне одночасно.';\n\n  @override\n  String get autoConnect => 'Авто підключення';\n\n  @override\n  String get autoRun => 'Авто запуск';\n\n  @override\n  String get autoUpdateHomeWidget =>\n      'Автоматичне оновлення віджетів на головному екрані';\n\n  @override\n  String get availableTabs => 'Доступні вкладки';\n\n  @override\n  String get backupEncrypted => 'Резервна копія зашифрована';\n\n  @override\n  String get backupNotEncrypted => 'Резервна копія не зашифрована';\n\n  @override\n  String get backupPassword => 'Пароль резервного копіювання';\n\n  @override\n  String get backupPasswordRemoved => 'Пароль резервного копіювання видалено';\n\n  @override\n  String get backupPasswordSet => 'Пароль резервного копіювання встановлено';\n\n  @override\n  String get backupPasswordTip =>\n      'Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.';\n\n  @override\n  String get backupPasswordWrong => 'Неправильний пароль резервного копіювання';\n\n  @override\n  String get backupTip =>\n      'Експортовані дані можуть бути зашифровані паролем. \\nБудь ласка, зберігайте їх у безпеці.';\n\n  @override\n  String get backupVersionNotMatch =>\n      'Версія резервного копіювання не збіглася.';\n\n  @override\n  String get bgRun => 'Запуск у фоновому режимі';\n\n  @override\n  String get bgRunTip =>\n      'Цей перемикач лише вказує на те, що програма намагатиметься працювати у фоновому режимі. Чи може вона працювати у фоновому режимі, залежить від прав доступу. Для AOSP-орієнтованих Android ROM, будь ласка, вимкніть \\\"Оптимізацію акумулятора\\\" в цьому додатку. Для MIUI / HyperOS, будь ласка, змініть політику економії енергії на \\\"Нескінченна\\\".';\n\n  @override\n  String get clearAllStatsContent =>\n      'Ви впевнені, що хочете очистити всю статистику з\\'єднань сервера? Цю дію не можна скасувати.';\n\n  @override\n  String get clearAllStatsTitle => 'Очистити всю статистику';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return 'Ви впевнені, що хочете очистити статистику з\\'єднань для сервера \\\"$serverName\\\"? Цю дію не можна скасувати.';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return 'Очистити статистику $serverName';\n  }\n\n  @override\n  String get clearThisServerStats => 'Очистити статистику цього сервера';\n\n  @override\n  String get compactDatabase => 'Стиснути базу даних';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return 'Розмір бази даних: $size\\n\\nЦе перебудує базу даних, щоб зменшити розмір файлу. Дані не будуть видалені.';\n  }\n\n  @override\n  String get closeAfterSave => 'Зберегти та закрити';\n\n  @override\n  String get collapseUITip =>\n      'Сховати довгі списки, що є у UI за замовчуванням';\n\n  @override\n  String get connectionDetails => 'Деталі з\\'єднання';\n\n  @override\n  String get connectionStats => 'Статистика з\\'єднань';\n\n  @override\n  String get connectionStatsDesc =>\n      'Переглянути коефіцієнт успішності підключення до сервера та історію';\n\n  @override\n  String get containerTrySudoTip =>\n      'Наприклад: У застосунку користувач це aaa, але Docker встановлений під користувачем root. У цьому випадку вам потрібно активувати цю опцію.';\n\n  @override\n  String get containerSudoPasswordRequired =>\n      'Для доступу до Docker потрібен пароль sudo. Будь ласка, введіть ваш пароль.';\n\n  @override\n  String get containerSudoPasswordIncorrect =>\n      'Пароль sudo неправильний або не дозволений. Будь ласка, спробуйте ще раз.';\n\n  @override\n  String get convert => 'Конвертувати';\n\n  @override\n  String get copyPath => 'Скопіювати шлях';\n\n  @override\n  String get cpuViewAsProgressTip =>\n      'Відобразити використання кожного процесора у вигляді стовпчикової діаграми (старий стиль)';\n\n  @override\n  String get cursorType => 'Тип курсора';\n\n  @override\n  String get customCmd => 'Користувацькі команди';\n\n  @override\n  String get customCmdHint => '\\\"Ім\\'я Команди\\\": \\\"Команда\\\"';\n\n  @override\n  String get deleteServers => 'Масове видалення серверів';\n\n  @override\n  String get desktopTerminalTip =>\n      'Команда для відкриття емулятора термінала під час запуску SSH-сеансів.';\n\n  @override\n  String get dirEmpty => 'Переконайтеся, що директорія пуста.';\n\n  @override\n  String get discoverSshServers => 'Виявити SSH сервери';\n\n  @override\n  String get discoveryFailed => 'Виявлення не вдалось';\n\n  @override\n  String get discoverySettings => 'Налаштування виявлення';\n\n  @override\n  String get discoverySummary => 'Підсумок виявлення';\n\n  @override\n  String get diskHealth => 'Стан диска';\n\n  @override\n  String get diskIgnorePath => 'Ігнорувати шлях для диска';\n\n  @override\n  String get displayCpuIndex => 'Відобразити індекс ЦП';\n\n  @override\n  String dl2Local(Object fileName) {\n    return 'Завантажити $fileName на локальний комп\\'ютер?';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      'Немає запущених контейнерів.\\nЦе може бути через:\\n- Користувача Docker, відмінного від користувача, налаштованого в додатку\\n- змінну оточення DOCKER_HOST, яка не була правильно зчитана. Ви можете виконати `echo \\$DOCKER_HOST` у терміналі, щоб побачити її значення.';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return 'Всього $count образів';\n  }\n\n  @override\n  String get dockerNotInstalled => 'Docker не встановлено';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount запущено, $stoppedCount контейнерів зупинено.';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count контейнер(и) запущено.';\n  }\n\n  @override\n  String get doubleColumnMode => 'Режим подвійної колонки';\n\n  @override\n  String get doubleColumnTip =>\n      'Ця опція лише активує функцію, чи можна її насправді включити, залежить від ширини пристрою';\n\n  @override\n  String get editVirtKeys => 'Редагувати віртуальні клавіші';\n\n  @override\n  String get editorHighlightTip =>\n      'Поточна підсвітка коду не ідеальна і може бути вимкнена для покращення.';\n\n  @override\n  String get enableMdns => 'Увімкнути mDNS';\n\n  @override\n  String get enableMdnsDesc =>\n      'Використовувати mDNS/Bonjour для виявлення SSH сервісів';\n\n  @override\n  String get envVars => 'Змінні середовища';\n\n  @override\n  String get experimentalFeature => 'Експериментальна функція';\n\n  @override\n  String get extraArgs => 'Додаткові аргументи';\n\n  @override\n  String get fallbackSshDest => 'Резервна SSH адреса';\n\n  @override\n  String get fdroidReleaseTip =>\n      'Якщо ви завантажили цей застосунок з F-Droid, рекомендується відключити цю опцію.';\n\n  @override\n  String get fgService => 'Служба переднього плану';\n\n  @override\n  String get fgServiceTip =>\n      'Після увімкнення деякі моделі пристроїв можуть вилітати. Вимкнення може призвести до того, що деякі моделі не зможуть підтримувати SSH-з\\'єднання у фоновому режимі. Будь ласка, дозвольте ServerBox права на сповіщення, фонову роботу та самопробудження в системних налаштуваннях.';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return 'Файл \\'$file\\' занадто великий ($size), макс $sizeMax';\n  }\n\n  @override\n  String get finishedAt => 'Завершено о';\n\n  @override\n  String get followSystem => 'Слідувати системі';\n\n  @override\n  String get fontSize => 'Розмір шрифту';\n\n  @override\n  String get fullScreen => 'Повноекранний режим';\n\n  @override\n  String get fullScreenJitter => 'Тремтіння в повноекранному режимі';\n\n  @override\n  String get fullScreenJitterHelp => 'Щоб уникнути вигоряння екрану';\n\n  @override\n  String get fullScreenTip =>\n      'Чи слід увімкнути повноекранний режим під час повороту пристрою в горизонтальне положення? Ця опція стосується лише вкладки сервера.';\n\n  @override\n  String get goBackQ => 'Повернутися назад?';\n\n  @override\n  String get goto => 'Перейти до';\n\n  @override\n  String get hideTitleBar => 'Сховати заголовок';\n\n  @override\n  String get highlight => 'Підсвітка коду';\n\n  @override\n  String get homeTabs => 'Домашні вкладки';\n\n  @override\n  String get homeTabsCustomizeDesc =>\n      'Налаштуйте, які вкладки відображаються на головній сторінці та їх порядок';\n\n  @override\n  String get homeWidgetUrlConfig =>\n      'Налаштувати URL віджета на головному екрані';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return 'Запит не вдався, код статусу: $code';\n  }\n\n  @override\n  String get ignoreCert => 'Ігнорувати сертифікат';\n\n  @override\n  String get image => 'Зображення';\n\n  @override\n  String get imagesList => 'Список зображень';\n\n  @override\n  String get installDockerWithUrl =>\n      'Будь ласка, спочатку встановіть Docker. (https://docs.docker.com/engine/install)';\n\n  @override\n  String get invalid => 'Недійсний';\n\n  @override\n  String get invalidHostFormat =>\n      'Недійсний формат хоста. Дозволено лише символи IPv4, IPv6 та домену.';\n\n  @override\n  String get jumpServer => 'Стрибковий Сервер';\n\n  @override\n  String get keepForeground => 'Тримати застосунок на передньому плані!';\n\n  @override\n  String get keepStatusWhenErr => 'Зберегати останній стан сервера';\n\n  @override\n  String get keepStatusWhenErrTip =>\n      'Тільки в разі виникнення помилки під час виконання скрипту';\n\n  @override\n  String get keyAuth => 'Аутентифікація ключем';\n\n  @override\n  String get lastFailure => 'Остання помилка';\n\n  @override\n  String get lastSuccess => 'Останній успіх';\n\n  @override\n  String get letterCache => 'Кешування букв';\n\n  @override\n  String get letterCacheTip =>\n      'Рекомендується відключити, але після вимкнення стане неможливим введення CJK (китайських, японських, корейських) символів.';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return 'Зроблено з ❤️ від $myGithub';\n  }\n\n  @override\n  String get max => 'макс.';\n\n  @override\n  String get maxConcurrency => 'Максимальна паралельність';\n\n  @override\n  String get maxRetryCount =>\n      'Кількість повторних спроб підключення до сервера';\n\n  @override\n  String get maxRetryCountEqual0 =>\n      'Знову і знову буде намагатися повторно підключитися.';\n\n  @override\n  String get min => 'мін.';\n\n  @override\n  String get more => 'Більше';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      'Включено: може відображатися під кожною карткою на вкладці Сервер. Вимкнено: може відображатися вгорі на сторінці деталей сервера.';\n\n  @override\n  String get needHomeDir =>\n      'Якщо ви користувач Synology, [дивіться тут](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Користувачі інших систем повинні знайти інформацію про те, як створити домашній каталог.';\n\n  @override\n  String get needRestart => 'Необхідно перезапустити застосунок';\n\n  @override\n  String get netViewType => 'Тип перегляду мережі';\n\n  @override\n  String get newContainer => 'Новий контейнер';\n\n  @override\n  String get noConnectionStatsData => 'Немає даних статистики з\\'єднань';\n\n  @override\n  String get noLineChart => 'Не використовувати лінійні діаграми';\n\n  @override\n  String get noLineChartForCpu => 'Не використовувати лінійні діаграми для ЦП';\n\n  @override\n  String get noPrivateKeyTip =>\n      'Приватного ключа немає, можливо, він був видалений або сталася помилка конфігурації.';\n\n  @override\n  String get noPromptAgain => 'Більше не запитувати';\n\n  @override\n  String get onServerDetailPage => 'На сторінці деталі сервера';\n\n  @override\n  String get onlyOneLine => 'Відображати лише в один рядок (прокрутка)';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 =>\n      'Працює лише тоді, коли кількість ядер перевищує 8';\n\n  @override\n  String get openLastPath => 'Відкрити останній шлях';\n\n  @override\n  String get openLastPathTip =>\n      'Для різних серверів будуть збережені різні логи. Записується шлях при виході';\n\n  @override\n  String get parseContainerStatsTip =>\n      'Парсинг статусу зайнятості Docker є відносно повільним.';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$percent% з $size';\n  }\n\n  @override\n  String get permission => 'Дозволи';\n\n  @override\n  String get pingInputIP => 'Будь ласка, введіть цільовий IP / Домен.';\n\n  @override\n  String get pingNoServer =>\n      'Немає сервера для пінгування.\\nБудь ласка, додайте сервер у вкладці `Сервер`.';\n\n  @override\n  String get plugInType => 'Тип вставки';\n\n  @override\n  String get preferDiskAmount => 'Пріоритетно показувати ємність диска';\n\n  @override\n  String get privateKey => 'Приватний ключ';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return 'Приватний ключ [$keyId] не знайдено.';\n  }\n\n  @override\n  String get pushToken => 'Надіслати токен';\n\n  @override\n  String get pveIgnoreCertTip =>\n      'Не рекомендується включати, будьте обережні з ризиками безпеки! Якщо ви використовуєте стандартний сертифікат від PVE, вам потрібно увімкнути цю опцію.';\n\n  @override\n  String get pveLoginFailed =>\n      'Не вдалося увійти. Неможливо пройти аутентифікацію за допомогою імені користувача/пароля з конфігурації сервера для входу Linux PAM.';\n\n  @override\n  String get pveVersionLow =>\n      'Ця функція наразі перебуває на стадії тестування та випробувалася лише на PVE 8+. Будь ласка, використовуйте її з обережністю.';\n\n  @override\n  String get read => 'Читати';\n\n  @override\n  String get recentConnections => 'Останні з\\'єднання';\n\n  @override\n  String get rememberPwdInMem => 'Запам\\'ятати пароль у пам\\'яті';\n\n  @override\n  String get rememberPwdInMemTip =>\n      'Використовується для контейнерів, призупинення тощо.';\n\n  @override\n  String get rememberWindowSize => 'Запам\\'ятати розмір вікна';\n\n  @override\n  String get remotePath => 'Віддалений шлях';\n\n  @override\n  String get result => 'Результат';\n\n  @override\n  String get rotateAngel => 'Кут повороту';\n\n  @override\n  String get sameIdServerExist => 'Сервер з таким ID вже існує';\n\n  @override\n  String get save => 'Зберегти';\n\n  @override\n  String get second => 'сек.';\n\n  @override\n  String get serverDetailOrder => 'Порядок віджетів на сторінці деталі';\n\n  @override\n  String get serverFuncBtns => 'Кнопки функцій сервера';\n\n  @override\n  String get serverOrder => 'Порядок сервера';\n\n  @override\n  String get serverTabRequired => 'Вкладку сервера не можна видалити';\n\n  @override\n  String get sftpDlPrepare => 'Підготовка до підключення...';\n\n  @override\n  String get sftpEditorTip =>\n      'Якщо порожньо, використовуйте вбудований редактор файлів програми. Якщо є значення, використовуйте редактор віддаленого сервера, наприклад, `vim` (рекомендується автоматично визначити відповідно до `EDITOR`).';\n\n  @override\n  String get sftpRmrDirSummary =>\n      'Використовуйте `rm -r`, щоб видалити папку в SFTP.';\n\n  @override\n  String get sftpSSHConnected => 'SFTP підключено';\n\n  @override\n  String get sftpShowFoldersFirst => 'Спочатку відображати директорії';\n\n  @override\n  String get showDistLogo => 'Показати логотип дистрибутива';\n\n  @override\n  String get size => 'Розмір';\n\n  @override\n  String get softWrap => 'М\\'ягкий перенос';\n\n  @override\n  String get specifyDev => 'Вказати пристрій';\n\n  @override\n  String get specifyDevTip =>\n      'Наприклад, статистика мережевого трафіку за замовчуванням є для всіх пристроїв. Ви можете вказати певний пристрій тут.';\n\n  @override\n  String get speed => 'Швидкість';\n\n  @override\n  String spentTime(Object time) {\n    return 'Витрачений час: $time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return 'Всі сервери вже існують (знайдено $duplicateCount дублікатів)';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount дублікатів буде пропущено';\n  }\n\n  @override\n  String get sshConfigFound => 'Ми знайшли SSH-конфігурацію у вашій системі';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return 'Знайдено $totalCount серверів';\n  }\n\n  @override\n  String get sshConfigImport => 'Імпорт SSH Конфігурації';\n\n  @override\n  String get sshConfigImportHelp =>\n      'Можна імпортувати лише базову інформацію, наприклад: IP/порт.';\n\n  @override\n  String get sshConfigImportPermission =>\n      'Чи хочете ви надати дозвіл на читання ~/.ssh/config та автоматичний імпорт налаштувань сервера?';\n\n  @override\n  String get sshConfigImportTip =>\n      'Пропозиція прочитати ~/.ssh/config при створенні першого сервера';\n\n  @override\n  String sshConfigImported(Object count) {\n    return 'Імпортовано $count серверів з SSH-конфігурації';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return 'SSH-ключ хоста для $serverName змінено. Продовжуйте лише якщо довіряєте цьому серверу.';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return 'Відбиток (MD5 Base64): $fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return 'Відбиток (MD5 hex): $fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'Тип ключа хоста SSH';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return 'Отримано новий SSH-ключ хоста від $serverName. Перевірте відбиток перед тим, як довіряти.';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return 'Збережений відбиток: $fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect =>\n      'Чи хочете ви вручну вибрати файл конфігурації SSH?';\n\n  @override\n  String get sshConfigNoServers => 'Сервери не знайдені в SSH-конфігурації';\n\n  @override\n  String get sshConfigPermissionDenied =>\n      'Неможливо отримати доступ до файлу конфігурації SSH через дозволи macOS.';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount серверів буде імпортовано';\n  }\n\n  @override\n  String get sshTermHelp =>\n      'Коли термінал прокрутний, горизонтальне проведення вибирає текст. Натискання кнопки клавіатури вмикає/вимикає клавіатуру. Іконка файлу відкриває поточний шлях SFTP. Кнопка буфера обміну копіює вміст, коли текст вибрано, і вставляє вміст з буфера обміну в термінал, коли текст не вибрано і є вміст у буфері обміну. Іконка коду вставляє фрагменти коду в термінал і виконує їх.';\n\n  @override\n  String sshTip(Object url) {\n    return 'Ця функція наразі в експериментальній стадії. Будь ласка, повідомте про помилки за адресою $url або приєднуйтеся до нашої розробки.';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff =>\n      'Автоматичне переключення віртуальних клавіш';\n\n  @override\n  String get stat => 'Статистика';\n\n  @override\n  String get supportFmtArgs => 'Підтримуються такі параметри форматування:';\n\n  @override\n  String get suspendTip =>\n      'Функція призупинення потребує адміністративних прав та підтримки systemd.';\n\n  @override\n  String switchTo(Object val) {\n    return 'Переключитися на $val';\n  }\n\n  @override\n  String get syncTip =>\n      'Може знадобитися перезапуск, щоб деякі зміни набрали чинності.';\n\n  @override\n  String get system => 'Система';\n\n  @override\n  String get tag => 'Теги';\n\n  @override\n  String get tapToStartDiscovery =>\n      'Натисніть кнопку пошуку, щоб виявити SSH сервери у вашій мережі';\n\n  @override\n  String get termFontSizeTip =>\n      'Це налаштування вплине на розмір терміналу (ширину та висоту). Ви можете масштабувати на сторінці терміналу, щоб налаштувати розмір шрифту поточної сесії.';\n\n  @override\n  String get textScaler => 'Масштабування тексту';\n\n  @override\n  String get textScalerTip =>\n      '1.0 => 100% (оригінальний розмір), працює лише на частині шрифта сторінки сервера, не рекомендується змінювати.';\n\n  @override\n  String get time => 'Час';\n\n  @override\n  String get times => 'Рази';\n\n  @override\n  String get trySudo => 'Спробуйте використовувати sudo';\n\n  @override\n  String get unknown => 'Невідомо';\n\n  @override\n  String get unkownConvertMode => 'Невідомий режим конвертації';\n\n  @override\n  String get update => 'Оновити';\n\n  @override\n  String get updateIntervalEqual0 =>\n      'Ви встановили 0, автоматичне оновлення не відбудеться.\\nНе можна розрахувати статус ЦП.';\n\n  @override\n  String get updateServerStatusInterval => 'Інтервал оновлення статусу сервера';\n\n  @override\n  String get upsideDown => 'Доверху дном';\n\n  @override\n  String get useCdn => 'Використання CDN';\n\n  @override\n  String get useCdnTip =>\n      'Нереспонсивним користувачам рекомендується використовувати CDN. Чи хочете ви його використовувати?';\n\n  @override\n  String get useNoPwd => 'Пароль не буде використовуватися';\n\n  @override\n  String get usePodmanByDefault => 'Використовувати Podman за замовчуванням';\n\n  @override\n  String get used => 'Використано';\n\n  @override\n  String get view => 'Переглянути';\n\n  @override\n  String get viewDetails => 'Переглянути деталі';\n\n  @override\n  String get viewErr => 'Переглянути помилку';\n\n  @override\n  String get virtKeyHelpClipboard =>\n      'Копіювати в буфер обміну, якщо вибраний термінал не порожній, в іншому випадку вставити вміст буфера обміну в термінал.';\n\n  @override\n  String get virtKeyHelpIME => 'Увімкнути/вимкнути клавіатуру';\n\n  @override\n  String get virtKeyHelpSFTP => 'Відкрити поточний каталог у SFTP.';\n\n  @override\n  String get waitConnection =>\n      'Будь ласка, зачекайте, доки з\\'єднання буде встановлено.';\n\n  @override\n  String get wakeLock => 'Залишити активним';\n\n  @override\n  String get watchNotPaired => 'Немає спарованого Apple Watch';\n\n  @override\n  String get webdavSettingEmpty => 'Налаштування WebDav порожнє';\n\n  @override\n  String get whenOpenApp => 'При відкритті програми';\n\n  @override\n  String get wolTip =>\n      'Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.';\n\n  @override\n  String get write => 'Записати';\n\n  @override\n  String get writeScriptFailTip =>\n      'Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.';\n\n  @override\n  String get writeScriptTip =>\n      'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \\n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';\n\n  @override\n  String get menuGitHubRepository => 'GitHub Repository';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      'Виявлено емуляцію Podman Docker. Будь ласка, переключіться на Podman у налаштуваннях.';\n}\n"
  },
  {
    "path": "lib/generated/l10n/l10n_zh.dart",
    "content": "// ignore: unused_import\nimport 'package:intl/intl.dart' as intl;\nimport 'l10n.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 aboutThanks => '感谢以下参与的各位。';\n\n  @override\n  String get acceptBeta => '接受测试版更新推送';\n\n  @override\n  String get addSystemPrivateKeyTip => '检测到暂无私钥，是否添加系统默认的私钥（~/.ssh/id_rsa）？';\n\n  @override\n  String get added2List => '已添加至任务列表';\n\n  @override\n  String get addr => '地址';\n\n  @override\n  String get alreadyLastDir => '已是顶级目录';\n\n  @override\n  String get askAi => '问 AI';\n\n  @override\n  String get askAiApiKey => 'API 密钥';\n\n  @override\n  String get askAiAwaitingResponse => '等待 AI 响应...';\n\n  @override\n  String get askAiBaseUrl => '基础 URL';\n\n  @override\n  String get askAiCommandInserted => '命令已插入终端';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return '请前往设置配置 $fields';\n  }\n\n  @override\n  String get askAiConfirmExecute => '执行前确认';\n\n  @override\n  String get askAiConversation => 'AI 对话';\n\n  @override\n  String get askAiDisclaimer => 'AI 可能会犯错，请谨慎使用。';\n\n  @override\n  String get askAiFollowUpHint => '继续提问...';\n\n  @override\n  String get askAiInsertTerminal => '插入终端';\n\n  @override\n  String get askAiNoResponse => '无回复内容';\n\n  @override\n  String get askAiRecommendedCommand => 'AI 推荐命令';\n\n  @override\n  String get askAiSelectedContent => '选中的内容';\n\n  @override\n  String get askAiUsageHint => '用于 SSH 终端';\n\n  @override\n  String get atLeastOneTab => '至少需要选择一个标签';\n\n  @override\n  String get authFailTip => '认证失败，请检查连接信息是否正确';\n\n  @override\n  String get autoBackupConflict => '仅可启用一个自动备份任务';\n\n  @override\n  String get autoConnect => '自动连接';\n\n  @override\n  String get autoRun => '自动运行';\n\n  @override\n  String get autoUpdateHomeWidget => '自动更新桌面小部件';\n\n  @override\n  String get availableTabs => '可用标签';\n\n  @override\n  String get backupEncrypted => '备份已加密';\n\n  @override\n  String get backupNotEncrypted => '备份未加密';\n\n  @override\n  String get backupPassword => '备份密码';\n\n  @override\n  String get backupPasswordRemoved => '备份密码已移除';\n\n  @override\n  String get backupPasswordSet => '备份密码已设置';\n\n  @override\n  String get backupPasswordTip => '设置密码以加密备份文件。留空则禁用加密。';\n\n  @override\n  String get backupPasswordWrong => '备份密码错误';\n\n  @override\n  String get backupTip => '导出数据可通过密码加密，请妥善保管。';\n\n  @override\n  String get backupVersionNotMatch => '备份版本不兼容，无法恢复';\n\n  @override\n  String get bgRun => '后台运行';\n\n  @override\n  String get bgRunTip =>\n      '此开关只代表程序会尝试在后台运行，具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”，MIUI / HyperOS 请将省电策略改为“无限制”。';\n\n  @override\n  String get clearAllStatsContent => '确定要清空所有服务器的连接统计数据吗？此操作无法撤销。';\n\n  @override\n  String get clearAllStatsTitle => '清空所有统计';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return '确定要清空服务器 \\\"$serverName\\\" 的连接统计数据吗？此操作无法撤销。';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return '清空 $serverName 统计';\n  }\n\n  @override\n  String get clearThisServerStats => '清空此服务器统计';\n\n  @override\n  String get compactDatabase => '压缩数据库';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return '数据库大小：$size\\n\\n此操作将重新组织数据库以减少体积，数据不会丢失。';\n  }\n\n  @override\n  String get closeAfterSave => '保存后关闭';\n\n  @override\n  String get collapseUITip => '是否默认折叠 UI 中的长列表';\n\n  @override\n  String get connectionDetails => '连接详情';\n\n  @override\n  String get connectionStats => '连接统计';\n\n  @override\n  String get connectionStatsDesc => '查看服务器连接成功率和历史记录';\n\n  @override\n  String get containerTrySudoTip =>\n      '例如：在应用内将用户设置为 aaa，但是 Docker 安装在root用户下，这时就需要启用此选项';\n\n  @override\n  String get containerSudoPasswordRequired => '需要 sudo 密码才能访问 Docker。请输入您的密码。';\n\n  @override\n  String get containerSudoPasswordIncorrect => 'sudo 密码错误或无权限。请重试。';\n\n  @override\n  String get convert => '转换';\n\n  @override\n  String get copyPath => '复制路径';\n\n  @override\n  String get cpuViewAsProgressTip => '以进度条样式显示每个 CPU 的使用率（旧版样式）';\n\n  @override\n  String get cursorType => '光标类型';\n\n  @override\n  String get customCmd => '自定义命令';\n\n  @override\n  String get customCmdHint => '\\\"命令名称\\\": \\\"命令\\\"';\n\n  @override\n  String get deleteServers => '批量删除服务器';\n\n  @override\n  String get desktopTerminalTip => '启动 SSH 连接所用的终端模拟器命令';\n\n  @override\n  String get dirEmpty => '请确保目录为空';\n\n  @override\n  String get discoverSshServers => '发现SSH服务器';\n\n  @override\n  String get discoveryFailed => '发现失败';\n\n  @override\n  String get discoverySettings => '发现设置';\n\n  @override\n  String get discoverySummary => '发现摘要';\n\n  @override\n  String get diskHealth => '磁盘健康';\n\n  @override\n  String get diskIgnorePath => '忽略的磁盘路径';\n\n  @override\n  String get displayCpuIndex => '显示 CPU 索引';\n\n  @override\n  String dl2Local(Object fileName) {\n    return '下载 $fileName 到本地？';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      '没有正在运行的容器。\\n这可能是因为：\\n- Docker 安装用户与 App 内配置的用户名不同\\n- 环境变量 DOCKER_HOST 没有被正确读取。可以通过在终端内运行 `echo \\$DOCKER_HOST` 来获取。';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '$count 个镜像';\n  }\n\n  @override\n  String get dockerNotInstalled => '未安装 Docker';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount 个正在运行, $stoppedCount 个已停止';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count 个容器正在运行';\n  }\n\n  @override\n  String get doubleColumnMode => '双列模式';\n\n  @override\n  String get doubleColumnTip => '此选项仅用于启用该功能，是否生效取决于设备宽度';\n\n  @override\n  String get editVirtKeys => '编辑虚拟按键';\n\n  @override\n  String get editorHighlightTip => '代码高亮功能可能影响性能，可选择关闭。';\n\n  @override\n  String get enableMdns => '启用mDNS';\n\n  @override\n  String get enableMdnsDesc => '使用mDNS/Bonjour发现SSH服务';\n\n  @override\n  String get envVars => '环境变量';\n\n  @override\n  String get experimentalFeature => '实验性功能';\n\n  @override\n  String get extraArgs => '额外参数';\n\n  @override\n  String get fallbackSshDest => '备选 SSH 目标';\n\n  @override\n  String get fdroidReleaseTip => '如果你是从 F-Droid 下载的本应用，推荐关闭此选项';\n\n  @override\n  String get fgService => '前台服务';\n\n  @override\n  String get fgServiceTip =>\n      '开启后，可能会导致部分机型闪退。关闭可能导致部分机型无法后台保持 SSH 连接。请在系统设置内允许 ServerBox 通知权限、后台运行、自我唤醒。';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return '文件 \\'$file\\' 过大 \\'$size\\'，超过了 $sizeMax';\n  }\n\n  @override\n  String get finishedAt => '完成于';\n\n  @override\n  String get followSystem => '跟随系统';\n\n  @override\n  String get fontSize => '字体大小';\n\n  @override\n  String get fullScreen => '全屏模式';\n\n  @override\n  String get fullScreenJitter => '全屏模式抖动';\n\n  @override\n  String get fullScreenJitterHelp => '用于防止屏幕烧屏';\n\n  @override\n  String get fullScreenTip => '当设备旋转为横屏时，是否开启全屏模式。此选项仅作用于服务器 Tab 页。';\n\n  @override\n  String get goBackQ => '返回？';\n\n  @override\n  String get goto => '前往';\n\n  @override\n  String get hideTitleBar => '隐藏标题栏';\n\n  @override\n  String get highlight => '代码高亮';\n\n  @override\n  String get homeTabs => '主页标签';\n\n  @override\n  String get homeTabsCustomizeDesc => '自定义主页上显示的标签及其顺序';\n\n  @override\n  String get homeWidgetUrlConfig => '桌面部件链接配置';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return '请求失败，状态码: $code';\n  }\n\n  @override\n  String get ignoreCert => '忽略证书';\n\n  @override\n  String get image => '镜像';\n\n  @override\n  String get imagesList => '镜像列表';\n\n  @override\n  String get installDockerWithUrl =>\n      '请先前往 https://docs.docker.com/engine/install 安装 Docker';\n\n  @override\n  String get invalid => '无效';\n\n  @override\n  String get invalidHostFormat => '主机格式无效，仅支持 IPv4、IPv6 和域名字符。';\n\n  @override\n  String get jumpServer => '跳板服务器';\n\n  @override\n  String get keepForeground => '请将应用保持在前台运行';\n\n  @override\n  String get keepStatusWhenErr => '保留上次的服务器状态';\n\n  @override\n  String get keepStatusWhenErrTip => '仅限于执行脚本出错';\n\n  @override\n  String get keyAuth => '密钥认证';\n\n  @override\n  String get lastFailure => '最后失败';\n\n  @override\n  String get lastSuccess => '最后成功';\n\n  @override\n  String get letterCache => '输入法字符缓存';\n\n  @override\n  String get letterCacheTip => '推荐关闭，但是关闭后无法输入 CJK 等文字';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return '用❤️制作 by $myGithub';\n  }\n\n  @override\n  String get max => '最大';\n\n  @override\n  String get maxConcurrency => '最大并发数';\n\n  @override\n  String get maxRetryCount => '服务器尝试重连次数';\n\n  @override\n  String get maxRetryCountEqual0 => '将无限次重试';\n\n  @override\n  String get min => '最小';\n\n  @override\n  String get more => '更多';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      '开启：可以在服务器 Tab 页的每个卡片下方显示。关闭：在服务器详情页顶部显示。';\n\n  @override\n  String get needHomeDir =>\n      '如果你是群晖用户，[看这里](https://kb.synology.cn/zh-cn/DSM/tutorial/ssh_could_not_chdir_to_home_directory)。其他系统用户，需搜索如何创建家目录（home directory）.';\n\n  @override\n  String get needRestart => '需要重启 App';\n\n  @override\n  String get netViewType => '网络视图类型';\n\n  @override\n  String get newContainer => '新建容器';\n\n  @override\n  String get noConnectionStatsData => '暂无连接统计数据';\n\n  @override\n  String get noLineChart => '不使用折线图';\n\n  @override\n  String get noLineChartForCpu => 'CPU 不使用折线图';\n\n  @override\n  String get noPrivateKeyTip => '私钥不存在，可能已被删除/配置错误';\n\n  @override\n  String get noPromptAgain => '不再提示';\n\n  @override\n  String get onServerDetailPage => '在服务器详情页';\n\n  @override\n  String get onlyOneLine => '仅显示为一行（可滚动）';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 => '仅当核心数大于 8 时生效';\n\n  @override\n  String get openLastPath => '打开上次的路径';\n\n  @override\n  String get openLastPathTip => '将为每台服务器记录其最后访问路径';\n\n  @override\n  String get parseContainerStatsTip => 'Docker 解析占用状态较为缓慢';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$size 的 $percent%';\n  }\n\n  @override\n  String get permission => '权限';\n\n  @override\n  String get pingInputIP => '请输入目标IP或域名';\n\n  @override\n  String get pingNoServer => '没有服务器可用于 Ping\\n请在服务器 tab 添加服务器后再试';\n\n  @override\n  String get plugInType => '插入类型';\n\n  @override\n  String get preferDiskAmount => '优先显示硬盘容量';\n\n  @override\n  String get privateKey => '私钥';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return '未找到私钥 [$keyId]。';\n  }\n\n  @override\n  String get pushToken => '消息推送 Token';\n\n  @override\n  String get pveIgnoreCertTip => '不推荐开启，注意安全隐患！如果你使用的 PVE 默认证书，需要开启该选项';\n\n  @override\n  String get pveLoginFailed => '登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。';\n\n  @override\n  String get pveVersionLow => '当前该功能处于测试阶段，仅在 PVE 8+ 上测试过，请谨慎使用';\n\n  @override\n  String get read => '读';\n\n  @override\n  String get recentConnections => '最近连接记录';\n\n  @override\n  String get rememberPwdInMem => '在内存中记住密码';\n\n  @override\n  String get rememberPwdInMemTip => '用于容器、挂起等';\n\n  @override\n  String get rememberWindowSize => '记住窗口大小';\n\n  @override\n  String get remotePath => '远端路径';\n\n  @override\n  String get result => '结果';\n\n  @override\n  String get rotateAngel => '旋转角度';\n\n  @override\n  String get sameIdServerExist => '已存在相同 id 的服务器';\n\n  @override\n  String get save => '保存';\n\n  @override\n  String get second => '秒';\n\n  @override\n  String get serverDetailOrder => '详情页部件顺序';\n\n  @override\n  String get serverFuncBtns => '服务器功能按钮';\n\n  @override\n  String get serverOrder => '服务器顺序';\n\n  @override\n  String get serverTabRequired => '服务器标签不能被移除';\n\n  @override\n  String get sftpDlPrepare => '准备连接至服务器...';\n\n  @override\n  String get sftpEditorTip =>\n      '如果为空, 使用App内置的文件编辑器. 如果有值, 这是用远程服务器的编辑器, 例如 `vim` (建议根据 `EDITOR` 自动获取).';\n\n  @override\n  String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 来删除文件夹';\n\n  @override\n  String get sftpSSHConnected => 'SFTP 已连接';\n\n  @override\n  String get sftpShowFoldersFirst => '文件夹显示在前';\n\n  @override\n  String get showDistLogo => '显示发行版 Logo';\n\n  @override\n  String get size => '大小';\n\n  @override\n  String get softWrap => '自动换行';\n\n  @override\n  String get specifyDev => '指定设备';\n\n  @override\n  String get specifyDevTip => '例如网络流量统计默认是所有设备，你可以在这里指定特定的设备';\n\n  @override\n  String get speed => '速度';\n\n  @override\n  String spentTime(Object time) {\n    return '耗时：$time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return '所有服务器已存在（发现 $duplicateCount 个重复项）';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '$duplicateCount 个重复项将被跳过';\n  }\n\n  @override\n  String get sshConfigFound => '我们在您的系统中发现了 SSH 配置。';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return '发现 $totalCount 个服务器';\n  }\n\n  @override\n  String get sshConfigImport => 'SSH 配置导入';\n\n  @override\n  String get sshConfigImportHelp => '只能导入基础信息，例如：IP/端口';\n\n  @override\n  String get sshConfigImportPermission => '是否允许读取 ~/.ssh/config 并自动导入服务器设置？';\n\n  @override\n  String get sshConfigImportTip => '首次创建服务器时提示读取 ~/.ssh/config';\n\n  @override\n  String sshConfigImported(Object count) {\n    return '从 SSH 配置导入了 $count 个服务器';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return '服务器 $serverName 的 SSH 主机密钥已更改，仅在信任该服务器时继续。';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return '指纹（MD5 Base64）：$fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return '指纹（MD5 十六进制）：$fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'SSH 主机密钥类型';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return '收到来自 $serverName 的新 SSH 主机密钥，在信任前请检查指纹。';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return '已存储的指纹：$fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect => '是否要手动选择 SSH 配置文件？';\n\n  @override\n  String get sshConfigNoServers => 'SSH 配置中未找到服务器';\n\n  @override\n  String get sshConfigPermissionDenied => '由于 macOS 权限限制，无法访问 SSH 配置文件。';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '$importCount 个服务器将被导入';\n  }\n\n  @override\n  String get sshTermHelp =>\n      '在终端可滚动时，横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容，在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。';\n\n  @override\n  String sshTip(Object url) {\n    return '该功能目前处于测试阶段。\\n\\n请在 $url 反馈问题，或者加入我们开发。';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff => '虚拟按键自动切换';\n\n  @override\n  String get stat => '统计';\n\n  @override\n  String get supportFmtArgs => '支持以下格式化参数：';\n\n  @override\n  String get suspendTip => 'suspend 功能需要 root 权限及 systemd 支持。';\n\n  @override\n  String switchTo(Object val) {\n    return '切换到 $val';\n  }\n\n  @override\n  String get syncTip => '可能需要重新启动，某些更改才能生效。';\n\n  @override\n  String get system => '系统';\n\n  @override\n  String get tag => '标签';\n\n  @override\n  String get tapToStartDiscovery => '点击搜索按钮发现网络中的SSH服务器';\n\n  @override\n  String get termFontSizeTip => '此设置会影响终端大小（宽和高）。可以在终端页面缩放来调整当前会话的字体大小';\n\n  @override\n  String get textScaler => '字体缩放';\n\n  @override\n  String get textScalerTip => '1.0 => 100%（原大小），仅作用于服务器页面部分字体，不建议修改。';\n\n  @override\n  String get time => '时间';\n\n  @override\n  String get times => '次';\n\n  @override\n  String get trySudo => '尝试使用 sudo';\n\n  @override\n  String get unknown => '未知';\n\n  @override\n  String get unkownConvertMode => '未知转换模式';\n\n  @override\n  String get update => '更新';\n\n  @override\n  String get updateIntervalEqual0 => '设置为 0 将不自动刷新服务器状态。\\n且无法计算 CPU 使用率。';\n\n  @override\n  String get updateServerStatusInterval => '服务器状态刷新间隔';\n\n  @override\n  String get upsideDown => '上下交换';\n\n  @override\n  String get useCdn => '使用 CDN';\n\n  @override\n  String get useCdnTip => '非中国大陆用户推荐使用 CDN，是否使用？';\n\n  @override\n  String get useNoPwd => '将会使用无密码';\n\n  @override\n  String get usePodmanByDefault => '默认使用 Podman';\n\n  @override\n  String get used => '已用';\n\n  @override\n  String get view => '视图';\n\n  @override\n  String get viewDetails => '查看详情';\n\n  @override\n  String get viewErr => '查看错误';\n\n  @override\n  String get virtKeyHelpClipboard => '如果终端有选中字符，则复制选中字符至剪切板，否则粘贴剪切板内容至终端。';\n\n  @override\n  String get virtKeyHelpIME => '打开/关闭键盘';\n\n  @override\n  String get virtKeyHelpSFTP => '在 SFTP 中打开当前路径。';\n\n  @override\n  String get waitConnection => '请等待连接建立';\n\n  @override\n  String get wakeLock => '保持唤醒';\n\n  @override\n  String get watchNotPaired => '没有已配对的 Apple Watch';\n\n  @override\n  String get webdavSettingEmpty => 'WebDav 设置项为空';\n\n  @override\n  String get whenOpenApp => '当打开 App 时';\n\n  @override\n  String get wolTip => '配置 WOL 后，每次连接服务器时将自动发送唤醒请求';\n\n  @override\n  String get write => '写';\n\n  @override\n  String get writeScriptFailTip => '写入脚本失败，可能是没有权限/目录不存在等';\n\n  @override\n  String get writeScriptTip =>\n      '在连接服务器后，会向 `~/.config/server_box` \\n | `/tmp/server_box` 写入脚本来监测系统状态，你可以审查脚本内容。';\n\n  @override\n  String get menuGitHubRepository => 'GitHub 仓库';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      '检测到 Podman Docker 仿真。请在设置中切换到 Podman。';\n}\n\n/// The translations for Chinese, as used in Taiwan (`zh_TW`).\nclass AppLocalizationsZhTw extends AppLocalizationsZh {\n  AppLocalizationsZhTw() : super('zh_TW');\n\n  @override\n  String get aboutThanks => '感謝以下參與的各位。';\n\n  @override\n  String get acceptBeta => '接受測試版更新推送';\n\n  @override\n  String get addSystemPrivateKeyTip => '偵測到尚無私鑰，是否要加入系統預設的私鑰（~/.ssh/id_rsa）？';\n\n  @override\n  String get added2List => '已新增至任務清單';\n\n  @override\n  String get addr => '位址';\n\n  @override\n  String get alreadyLastDir => '已是頂層目錄';\n\n  @override\n  String get askAi => '詢問 AI';\n\n  @override\n  String get askAiApiKey => 'API 金鑰';\n\n  @override\n  String get askAiAwaitingResponse => '等待 AI 回應...';\n\n  @override\n  String get askAiBaseUrl => '基礎 URL';\n\n  @override\n  String get askAiCommandInserted => '指令已插入終端機';\n\n  @override\n  String askAiConfigMissing(Object fields) {\n    return '請前往設定配置 $fields';\n  }\n\n  @override\n  String get askAiConfirmExecute => '執行前確認';\n\n  @override\n  String get askAiConversation => 'AI 對話';\n\n  @override\n  String get askAiDisclaimer => 'AI 可能會犯錯，請謹慎使用。';\n\n  @override\n  String get askAiFollowUpHint => '繼續提問...';\n\n  @override\n  String get askAiInsertTerminal => '插入終端機';\n\n  @override\n  String get askAiNoResponse => '無回覆內容';\n\n  @override\n  String get askAiRecommendedCommand => 'AI 推薦指令';\n\n  @override\n  String get askAiSelectedContent => '選取的內容';\n\n  @override\n  String get askAiUsageHint => '於 SSH 終端機中使用';\n\n  @override\n  String get atLeastOneTab => '至少需要選擇一個標籤';\n\n  @override\n  String get authFailTip => '認證失敗，請檢查連線資訊是否正確';\n\n  @override\n  String get autoBackupConflict => '僅能啟用一項自動備份任務';\n\n  @override\n  String get autoConnect => '自動連線';\n\n  @override\n  String get autoRun => '自動執行';\n\n  @override\n  String get autoUpdateHomeWidget => '自動更新桌面小工具';\n\n  @override\n  String get availableTabs => '可用標籤';\n\n  @override\n  String get backupEncrypted => '備份已加密';\n\n  @override\n  String get backupNotEncrypted => '備份未加密';\n\n  @override\n  String get backupPassword => '備份密碼';\n\n  @override\n  String get backupPasswordRemoved => '備份密碼已移除';\n\n  @override\n  String get backupPasswordSet => '備份密碼已設定';\n\n  @override\n  String get backupPasswordTip => '設定密碼來加密備份檔案。留空則停用加密。';\n\n  @override\n  String get backupPasswordWrong => '備份密碼錯誤';\n\n  @override\n  String get backupTip => '匯出的資料可透過密碼加密，請妥善保管。';\n\n  @override\n  String get backupVersionNotMatch => '備份版本不相容，無法還原';\n\n  @override\n  String get bgRun => '背景執行';\n\n  @override\n  String get bgRunTip =>\n      '此開關僅代表程式會嘗試於背景執行，能否成功取決於系統權限。在原生 Android 上，請關閉本應用的「電池最佳化」；在 MIUI / HyperOS 上，請將省電策略調整為「無限制」。';\n\n  @override\n  String get clearAllStatsContent => '確定要清空所有伺服器的連線統計資料嗎？此操作無法撤銷。';\n\n  @override\n  String get clearAllStatsTitle => '清空所有統計';\n\n  @override\n  String clearServerStatsContent(Object serverName) {\n    return '確定要清空伺服器 \\\"$serverName\\\" 的連線統計資料嗎？此操作無法撤銷。';\n  }\n\n  @override\n  String clearServerStatsTitle(Object serverName) {\n    return '清空 $serverName 統計';\n  }\n\n  @override\n  String get clearThisServerStats => '清空此伺服器統計';\n\n  @override\n  String get compactDatabase => '壓縮資料庫';\n\n  @override\n  String compactDatabaseContent(Object size) {\n    return '資料庫大小：$size\\n\\n此操作將重新組織資料庫以減少體積，資料不會遺失。';\n  }\n\n  @override\n  String get closeAfterSave => '儲存後關閉';\n\n  @override\n  String get collapseUITip => '是否預設折疊 UI 中存在的長列表';\n\n  @override\n  String get connectionDetails => '連線詳情';\n\n  @override\n  String get connectionStats => '連線統計';\n\n  @override\n  String get connectionStatsDesc => '檢視伺服器連線成功率和歷史記錄';\n\n  @override\n  String get containerTrySudoTip =>\n      '例如：App 內設定使用者為 aaa，但是 Docker 安裝在 root 使用者，這時就需要開啟此選項';\n\n  @override\n  String get containerSudoPasswordRequired => '需要 sudo 密碼才能存取 Docker。請輸入您的密碼。';\n\n  @override\n  String get containerSudoPasswordIncorrect => 'sudo 密碼錯誤或無權限。請重試。';\n\n  @override\n  String get convert => '轉換';\n\n  @override\n  String get copyPath => '複製路徑';\n\n  @override\n  String get cpuViewAsProgressTip => '以進度條樣式顯示每個CPU的使用率（舊版樣式）';\n\n  @override\n  String get cursorType => '游標類型';\n\n  @override\n  String get customCmd => '自訂指令';\n\n  @override\n  String get customCmdHint => '\\\"指令名稱\\\": \\\"指令\\\"';\n\n  @override\n  String get deleteServers => '大量刪除伺服器';\n\n  @override\n  String get desktopTerminalTip => '啟動 SSH 連線時用於打開終端機模擬器的指令。';\n\n  @override\n  String get dirEmpty => '請確保目錄為空';\n\n  @override\n  String get discoverSshServers => '發現SSH服務器';\n\n  @override\n  String get discoveryFailed => '發現失敗';\n\n  @override\n  String get discoverySettings => '發現設定';\n\n  @override\n  String get discoverySummary => '發現摘要';\n\n  @override\n  String get diskHealth => '磁碟健康';\n\n  @override\n  String get diskIgnorePath => '忽略的磁碟路徑';\n\n  @override\n  String get displayCpuIndex => '顯示 CPU 索引';\n\n  @override\n  String dl2Local(Object fileName) {\n    return '下載 $fileName 到本地？';\n  }\n\n  @override\n  String get dockerEmptyRunningItems =>\n      '沒有正在執行的容器。\\n這可能是因為：\\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\\n- 環境變數 DOCKER_HOST 沒有被正確讀取。你可以通過在終端機內執行 `echo \\$DOCKER_HOST` 來獲取。';\n\n  @override\n  String dockerImagesFmt(Object count) {\n    return '$count 個映像檔';\n  }\n\n  @override\n  String get dockerNotInstalled => '未安裝 Docker';\n\n  @override\n  String dockerStatusRunningAndStoppedFmt(\n    Object runningCount,\n    Object stoppedCount,\n  ) {\n    return '$runningCount 個正在執行, $stoppedCount 個已停止';\n  }\n\n  @override\n  String dockerStatusRunningFmt(Object count) {\n    return '$count 個容器正在執行';\n  }\n\n  @override\n  String get doubleColumnMode => '雙列模式';\n\n  @override\n  String get doubleColumnTip => '此選項僅用於啟用此功能，是否生效取決於裝置寬度';\n\n  @override\n  String get editVirtKeys => '編輯虛擬按鍵';\n\n  @override\n  String get editorHighlightTip => '程式碼高亮功能可能影響效能，可選擇性關閉。';\n\n  @override\n  String get enableMdns => '啟用mDNS';\n\n  @override\n  String get enableMdnsDesc => '使用mDNS/Bonjour發現SSH服務';\n\n  @override\n  String get envVars => '環境變數';\n\n  @override\n  String get experimentalFeature => '實驗性功能';\n\n  @override\n  String get extraArgs => '額外參數';\n\n  @override\n  String get fallbackSshDest => '備選 SSH 目標';\n\n  @override\n  String get fdroidReleaseTip => '如果你是從 F-Droid 下載的本App，推薦關閉此選項';\n\n  @override\n  String get fgService => '前台服務';\n\n  @override\n  String get fgServiceTip =>\n      '開啟後，可能會導致部分機型閃退。關閉可能導致部分機型無法背景保持 SSH 連線。請在系統設定內允許 ServerBox 通知權限、背景執行、自我喚醒。';\n\n  @override\n  String fileTooLarge(Object file, Object size, Object sizeMax) {\n    return '檔案 \\'$file\\' 過大 \\'$size\\'，超過了 $sizeMax';\n  }\n\n  @override\n  String get finishedAt => '完成於';\n\n  @override\n  String get followSystem => '跟隨系統';\n\n  @override\n  String get fontSize => '字型大小';\n\n  @override\n  String get fullScreen => '全螢幕模式';\n\n  @override\n  String get fullScreenJitter => '全螢幕模式抖動';\n\n  @override\n  String get fullScreenJitterHelp => '防止螢幕烙印';\n\n  @override\n  String get fullScreenTip => '當設備旋轉為橫向時，是否開啟全螢幕模式？此選項僅適用於伺服器分頁。';\n\n  @override\n  String get goBackQ => '返回？';\n\n  @override\n  String get goto => '前往';\n\n  @override\n  String get hideTitleBar => '隱藏標題欄';\n\n  @override\n  String get highlight => '程式碼標記';\n\n  @override\n  String get homeTabs => '主頁標籤';\n\n  @override\n  String get homeTabsCustomizeDesc => '自訂主頁上顯示的標籤及其順序';\n\n  @override\n  String get homeWidgetUrlConfig => '桌面小工具連結配置';\n\n  @override\n  String httpFailedWithCode(Object code) {\n    return '請求失敗，狀態碼：$code';\n  }\n\n  @override\n  String get ignoreCert => '忽略憑證';\n\n  @override\n  String get image => '映像檔';\n\n  @override\n  String get imagesList => '映像檔列表';\n\n  @override\n  String get installDockerWithUrl =>\n      '請先前往 https://docs.docker.com/engine/install 安裝 Docker';\n\n  @override\n  String get invalid => '無效';\n\n  @override\n  String get invalidHostFormat => '主機格式無效，僅支援 IPv4、IPv6 和網域字元。';\n\n  @override\n  String get jumpServer => '跳板伺服器';\n\n  @override\n  String get keepForeground => '請讓 App 保持在前景執行';\n\n  @override\n  String get keepStatusWhenErr => '保留上次的伺服器狀態';\n\n  @override\n  String get keepStatusWhenErrTip => '僅在執行腳本出錯時';\n\n  @override\n  String get keyAuth => '金鑰認證';\n\n  @override\n  String get lastFailure => '最後失敗';\n\n  @override\n  String get lastSuccess => '最後成功';\n\n  @override\n  String get letterCache => '輸入法字符快取';\n\n  @override\n  String get letterCacheTip => '建議關閉，但關閉後將無法輸入 CJK 等文字。';\n\n  @override\n  String madeWithLove(Object myGithub) {\n    return '用❤️製作 by $myGithub';\n  }\n\n  @override\n  String get max => '最大';\n\n  @override\n  String get maxConcurrency => '最大並發數';\n\n  @override\n  String get maxRetryCount => '伺服器嘗試重連次數';\n\n  @override\n  String get maxRetryCountEqual0 => '將無限次重試';\n\n  @override\n  String get min => '最小';\n\n  @override\n  String get more => '更多';\n\n  @override\n  String get moveOutServerFuncBtnsHelp =>\n      '開啟：可以在伺服器 Tab 頁的每個卡片下方顯示。關閉：在伺服器詳情頁頂部顯示。';\n\n  @override\n  String get needHomeDir =>\n      '如果你是群暉使用者，[看這裡](https://kb.synology.com/DSM/tutorial/user_enable_home_service)。其他系統使用者，需搜尋如何建立家目錄（home directory）。';\n\n  @override\n  String get needRestart => '需要重開 App';\n\n  @override\n  String get netViewType => '網路檢視類型';\n\n  @override\n  String get newContainer => '新建容器';\n\n  @override\n  String get noConnectionStatsData => '暫無連線統計資料';\n\n  @override\n  String get noLineChart => '不使用折線圖';\n\n  @override\n  String get noLineChartForCpu => 'CPU 不使用折線圖';\n\n  @override\n  String get noPrivateKeyTip => '私鑰不存在，可能已被刪除/配置錯誤。';\n\n  @override\n  String get noPromptAgain => '不再提示';\n\n  @override\n  String get onServerDetailPage => '在伺服器詳情頁';\n\n  @override\n  String get onlyOneLine => '僅顯示為一行（可捲動）';\n\n  @override\n  String get onlyWhenCoreBiggerThan8 => '僅當核心數大於 8 時生效';\n\n  @override\n  String get openLastPath => '打開上次的路徑';\n\n  @override\n  String get openLastPathTip => '將為每台伺服器紀錄其最後存取路徑';\n\n  @override\n  String get parseContainerStatsTip => 'Docker 解析消耗狀態較為緩慢';\n\n  @override\n  String percentOfSize(Object percent, Object size) {\n    return '$size 的 $percent%';\n  }\n\n  @override\n  String get permission => '權限';\n\n  @override\n  String get pingInputIP => '請輸入目標 IP 或域名';\n\n  @override\n  String get pingNoServer => '沒有伺服器可用於 Ping\\n請在伺服器 Tab 新增伺服器後再試';\n\n  @override\n  String get plugInType => '插入類型';\n\n  @override\n  String get preferDiskAmount => '優先顯示硬碟容量';\n\n  @override\n  String get privateKey => '私鑰';\n\n  @override\n  String privateKeyNotFoundFmt(Object keyId) {\n    return '未找到私鑰 [$keyId]。';\n  }\n\n  @override\n  String get pushToken => '消息推送 Token';\n\n  @override\n  String get pveIgnoreCertTip => '不建議啟用，請注意安全風險！如果您使用的是 PVE 的預設憑證，則需要啟用此選項。';\n\n  @override\n  String get pveLoginFailed => '登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。';\n\n  @override\n  String get pveVersionLow => '此功能目前處於測試階段，僅在 PVE 8+ 上進行過測試。請謹慎使用。';\n\n  @override\n  String get read => '讀取';\n\n  @override\n  String get recentConnections => '最近連線記錄';\n\n  @override\n  String get rememberPwdInMem => '在記憶體中記住密碼';\n\n  @override\n  String get rememberPwdInMemTip => '用於容器、暫停等';\n\n  @override\n  String get rememberWindowSize => '記住視窗大小';\n\n  @override\n  String get remotePath => '遠端路徑';\n\n  @override\n  String get result => '結果';\n\n  @override\n  String get rotateAngel => '旋轉角度';\n\n  @override\n  String get sameIdServerExist => '已存在相同 ID 的伺服器';\n\n  @override\n  String get save => '儲存';\n\n  @override\n  String get second => '秒';\n\n  @override\n  String get serverDetailOrder => '詳情頁部件順序';\n\n  @override\n  String get serverFuncBtns => '伺服器功能按鈕';\n\n  @override\n  String get serverOrder => '伺服器順序';\n\n  @override\n  String get serverTabRequired => '服務器標籤不能被移除';\n\n  @override\n  String get sftpDlPrepare => '準備連線至伺服器...';\n\n  @override\n  String get sftpEditorTip =>\n      '如果為空, 使用App內建的檔案編輯器。如果有值, 則使用遠端伺服器的編輯器, 例如 `vim`（建議根據 `EDITOR` 自動獲取）。';\n\n  @override\n  String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除檔案夾';\n\n  @override\n  String get sftpSSHConnected => 'SFTP 已連線';\n\n  @override\n  String get sftpShowFoldersFirst => '資料夾顯示在前';\n\n  @override\n  String get showDistLogo => '顯示發行版 Logo';\n\n  @override\n  String get size => '大小';\n\n  @override\n  String get softWrap => '軟換行';\n\n  @override\n  String get specifyDev => '指定裝置';\n\n  @override\n  String get specifyDevTip => '例如網路流量統計預設是所有裝置，你可以在這裡指定特定的裝置。';\n\n  @override\n  String get speed => '速度';\n\n  @override\n  String spentTime(Object time) {\n    return '耗時：$time';\n  }\n\n  @override\n  String sshConfigAllExist(Object duplicateCount) {\n    return '所有伺服器均已存在（發現$duplicateCount個重複項）';\n  }\n\n  @override\n  String sshConfigDuplicatesSkipped(Object duplicateCount) {\n    return '將跳過$duplicateCount個重複項';\n  }\n\n  @override\n  String get sshConfigFound => '我們在您的系統中發現了SSH設定';\n\n  @override\n  String sshConfigFoundServers(Object totalCount) {\n    return '發現$totalCount個伺服器';\n  }\n\n  @override\n  String get sshConfigImport => '匯入SSH設定';\n\n  @override\n  String get sshConfigImportHelp => '只能匯入基礎資訊，例如：IP/端口。';\n\n  @override\n  String get sshConfigImportPermission => '您是否希望允許讀取 ~/.ssh/config 並自動匯入伺服器設定？';\n\n  @override\n  String get sshConfigImportTip => '在建立第一個伺服器時提示讀取 ~/.ssh/config';\n\n  @override\n  String sshConfigImported(Object count) {\n    return '已從SSH設定匯入$count個伺服器';\n  }\n\n  @override\n  String sshHostKeyChangedDesc(Object serverName) {\n    return '伺服器 $serverName 的 SSH 主機金鑰已變更，僅在信任該伺服器時繼續。';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Base64(Object fingerprint) {\n    return '指紋（MD5 Base64）：$fingerprint';\n  }\n\n  @override\n  String sshHostKeyFingerprintMd5Hex(Object fingerprint) {\n    return '指紋（MD5 十六進位）：$fingerprint';\n  }\n\n  @override\n  String get sshHostKeyType => 'SSH 主機金鑰類型';\n\n  @override\n  String sshHostKeyNewDesc(Object serverName) {\n    return '收到來自 $serverName 的新 SSH 主機金鑰，信任前請先檢查指紋。';\n  }\n\n  @override\n  String sshHostKeyStoredFingerprint(Object fingerprint) {\n    return '已儲存的指紋：$fingerprint';\n  }\n\n  @override\n  String get sshConfigManualSelect => '是否要手動選擇 SSH 設定檔案？';\n\n  @override\n  String get sshConfigNoServers => 'SSH設定中未找到伺服器';\n\n  @override\n  String get sshConfigPermissionDenied => '由於 macOS 權限限制，無法存取 SSH 設定檔案。';\n\n  @override\n  String sshConfigServersToImport(Object importCount) {\n    return '將匯入$importCount個伺服器';\n  }\n\n  @override\n  String get sshTermHelp =>\n      '在終端機可捲動時，橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容，在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。';\n\n  @override\n  String sshTip(Object url) {\n    return '該功能目前處於測試階段。\\n\\n請在 $url 回饋問題，或者加入我們開發。';\n  }\n\n  @override\n  String get sshVirtualKeyAutoOff => '虛擬按鍵自動切換';\n\n  @override\n  String get stat => '統計';\n\n  @override\n  String get supportFmtArgs => '支援以下格式化參數：';\n\n  @override\n  String get suspendTip => 'suspend 功能需要 root 權限及 systemd 支援。';\n\n  @override\n  String switchTo(Object val) {\n    return '切換到 $val';\n  }\n\n  @override\n  String get syncTip => '可能需要重新啟動，某些更改才能生效。';\n\n  @override\n  String get system => '系統';\n\n  @override\n  String get tag => '標籤';\n\n  @override\n  String get tapToStartDiscovery => '點擊搜尋按鈕發現網路中的SSH服務器';\n\n  @override\n  String get termFontSizeTip => '此設定將影響終端機大小（寬度和高度）。您可以在終端機頁面縮放，來調整目前會話的字型大小。';\n\n  @override\n  String get textScaler => '字型縮放';\n\n  @override\n  String get textScalerTip => '1.0 => 100%（原大小），僅作用於伺服器頁面部分字型，不建議修改。';\n\n  @override\n  String get time => '時間';\n\n  @override\n  String get times => '次';\n\n  @override\n  String get trySudo => '嘗試使用 sudo';\n\n  @override\n  String get unknown => '未知';\n\n  @override\n  String get unkownConvertMode => '未知轉換模式';\n\n  @override\n  String get update => '更新';\n\n  @override\n  String get updateIntervalEqual0 => '設定為 0 將不自動刷新伺服器狀態，\\n也無法計算 CPU 使用率。';\n\n  @override\n  String get updateServerStatusInterval => '伺服器狀態更新間隔';\n\n  @override\n  String get upsideDown => '上下交換';\n\n  @override\n  String get useCdn => '使用 CDN';\n\n  @override\n  String get useCdnTip => '非中國使用者建議使用 CDN，是否使用？';\n\n  @override\n  String get useNoPwd => '將使用無密碼';\n\n  @override\n  String get usePodmanByDefault => '預設使用 Podman';\n\n  @override\n  String get used => '已使用';\n\n  @override\n  String get view => '檢視';\n\n  @override\n  String get viewDetails => '檢視詳情';\n\n  @override\n  String get viewErr => '查看錯誤';\n\n  @override\n  String get virtKeyHelpClipboard => '如果終端機有選中字元，則復製選中字元至剪貼簿，否則貼上剪貼簿內容至終端機。';\n\n  @override\n  String get virtKeyHelpIME => '打開/關閉鍵盤';\n\n  @override\n  String get virtKeyHelpSFTP => '在 SFTP 中打開目前路徑。';\n\n  @override\n  String get waitConnection => '請等待連線建立';\n\n  @override\n  String get wakeLock => '保持喚醒';\n\n  @override\n  String get watchNotPaired => '沒有已配對的 Apple Watch';\n\n  @override\n  String get webdavSettingEmpty => 'WebDav 設定項爲空';\n\n  @override\n  String get whenOpenApp => '當打開 App 時';\n\n  @override\n  String get wolTip => '設定 WOL 後，每次連線伺服器時將自動發送喚醒請求';\n\n  @override\n  String get write => '寫入';\n\n  @override\n  String get writeScriptFailTip => '寫入腳本失敗，可能是沒有權限/目錄不存在等。';\n\n  @override\n  String get writeScriptTip =>\n      '連線到伺服器後，將會在 `~/.config/server_box` \\n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';\n\n  @override\n  String get podmanDockerEmulationDetected =>\n      '檢測到 Podman Docker 仿真。請在設定中切換到 Podman。';\n}\n"
  },
  {
    "path": "lib/hive/hive_adapters.dart",
    "content": "import 'package:hive_ce/hive.dart';\nimport 'package:server_box/data/model/app/menu/server_func.dart';\nimport 'package:server_box/data/model/app/net_view.dart';\nimport 'package:server_box/data/model/server/custom.dart';\nimport 'package:server_box/data/model/server/private_key_info.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/snippet.dart';\nimport 'package:server_box/data/model/server/system.dart';\nimport 'package:server_box/data/model/server/wol_cfg.dart';\nimport 'package:server_box/data/model/ssh/virtual_key.dart';\n\n@GenerateAdapters([\n  AdapterSpec<PrivateKeyInfo>(),\n  AdapterSpec<Snippet>(),\n  AdapterSpec<Spi>(),\n  AdapterSpec<VirtKey>(),\n  AdapterSpec<NetViewType>(),\n  AdapterSpec<ServerFuncBtn>(),\n  AdapterSpec<ServerCustom>(),\n  AdapterSpec<WakeOnLanCfg>(),\n  AdapterSpec<SystemType>(),\n])\npart 'hive_adapters.g.dart';\n"
  },
  {
    "path": "lib/hive/hive_adapters.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'hive_adapters.dart';\n\n// **************************************************************************\n// AdaptersGenerator\n// **************************************************************************\n\nclass PrivateKeyInfoAdapter extends TypeAdapter<PrivateKeyInfo> {\n  @override\n  final typeId = 1;\n\n  @override\n  PrivateKeyInfo read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return PrivateKeyInfo(id: fields[0] as String, key: fields[1] as String);\n  }\n\n  @override\n  void write(BinaryWriter writer, PrivateKeyInfo obj) {\n    writer\n      ..writeByte(2)\n      ..writeByte(0)\n      ..write(obj.id)\n      ..writeByte(1)\n      ..write(obj.key);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is PrivateKeyInfoAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass SnippetAdapter extends TypeAdapter<Snippet> {\n  @override\n  final typeId = 2;\n\n  @override\n  Snippet read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return Snippet(\n      name: fields[0] as String,\n      script: fields[1] as String,\n      tags: (fields[2] as List?)?.cast<String>(),\n      note: fields[3] as String?,\n      autoRunOn: (fields[4] as List?)?.cast<String>(),\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, Snippet obj) {\n    writer\n      ..writeByte(5)\n      ..writeByte(0)\n      ..write(obj.name)\n      ..writeByte(1)\n      ..write(obj.script)\n      ..writeByte(2)\n      ..write(obj.tags)\n      ..writeByte(3)\n      ..write(obj.note)\n      ..writeByte(4)\n      ..write(obj.autoRunOn);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is SnippetAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass SpiAdapter extends TypeAdapter<Spi> {\n  @override\n  final typeId = 3;\n\n  @override\n  Spi read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return Spi(\n      name: fields[0] as String,\n      ip: fields[1] as String,\n      port: (fields[2] as num).toInt(),\n      user: fields[3] as String,\n      pwd: fields[4] as String?,\n      keyId: fields[5] as String?,\n      tags: (fields[6] as List?)?.cast<String>(),\n      alterUrl: fields[7] as String?,\n      autoConnect: fields[8] == null ? true : fields[8] as bool,\n      jumpId: fields[9] as String?,\n      custom: fields[10] as ServerCustom?,\n      wolCfg: fields[11] as WakeOnLanCfg?,\n      envs: (fields[12] as Map?)?.cast<String, String>(),\n      id: fields[13] == null ? '' : fields[13] as String,\n      customSystemType: fields[14] as SystemType?,\n      disabledCmdTypes: (fields[15] as List?)?.cast<String>(),\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, Spi obj) {\n    writer\n      ..writeByte(16)\n      ..writeByte(0)\n      ..write(obj.name)\n      ..writeByte(1)\n      ..write(obj.ip)\n      ..writeByte(2)\n      ..write(obj.port)\n      ..writeByte(3)\n      ..write(obj.user)\n      ..writeByte(4)\n      ..write(obj.pwd)\n      ..writeByte(5)\n      ..write(obj.keyId)\n      ..writeByte(6)\n      ..write(obj.tags)\n      ..writeByte(7)\n      ..write(obj.alterUrl)\n      ..writeByte(8)\n      ..write(obj.autoConnect)\n      ..writeByte(9)\n      ..write(obj.jumpId)\n      ..writeByte(10)\n      ..write(obj.custom)\n      ..writeByte(11)\n      ..write(obj.wolCfg)\n      ..writeByte(12)\n      ..write(obj.envs)\n      ..writeByte(13)\n      ..write(obj.id)\n      ..writeByte(14)\n      ..write(obj.customSystemType)\n      ..writeByte(15)\n      ..write(obj.disabledCmdTypes);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is SpiAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass VirtKeyAdapter extends TypeAdapter<VirtKey> {\n  @override\n  final typeId = 4;\n\n  @override\n  VirtKey read(BinaryReader reader) {\n    switch (reader.readByte()) {\n      case 0:\n        return VirtKey.esc;\n      case 1:\n        return VirtKey.alt;\n      case 2:\n        return VirtKey.home;\n      case 3:\n        return VirtKey.up;\n      case 4:\n        return VirtKey.end;\n      case 5:\n        return VirtKey.sftp;\n      case 6:\n        return VirtKey.snippet;\n      case 7:\n        return VirtKey.tab;\n      case 8:\n        return VirtKey.ctrl;\n      case 9:\n        return VirtKey.left;\n      case 10:\n        return VirtKey.down;\n      case 11:\n        return VirtKey.right;\n      case 12:\n        return VirtKey.clipboard;\n      case 13:\n        return VirtKey.ime;\n      case 14:\n        return VirtKey.pgup;\n      case 15:\n        return VirtKey.pgdn;\n      case 16:\n        return VirtKey.slash;\n      case 17:\n        return VirtKey.backSlash;\n      case 18:\n        return VirtKey.underscore;\n      case 19:\n        return VirtKey.plus;\n      case 20:\n        return VirtKey.equal;\n      case 21:\n        return VirtKey.minus;\n      case 22:\n        return VirtKey.parenLeft;\n      case 23:\n        return VirtKey.parenRight;\n      case 24:\n        return VirtKey.bracketLeft;\n      case 25:\n        return VirtKey.bracketRight;\n      case 26:\n        return VirtKey.braceLeft;\n      case 27:\n        return VirtKey.braceRight;\n      case 28:\n        return VirtKey.chevronLeft;\n      case 29:\n        return VirtKey.chevronRight;\n      case 30:\n        return VirtKey.colon;\n      case 31:\n        return VirtKey.semicolon;\n      case 32:\n        return VirtKey.f1;\n      case 33:\n        return VirtKey.f2;\n      case 34:\n        return VirtKey.f3;\n      case 35:\n        return VirtKey.f4;\n      case 36:\n        return VirtKey.f5;\n      case 37:\n        return VirtKey.f6;\n      case 38:\n        return VirtKey.f7;\n      case 39:\n        return VirtKey.f8;\n      case 40:\n        return VirtKey.f9;\n      case 41:\n        return VirtKey.f10;\n      case 42:\n        return VirtKey.f11;\n      case 43:\n        return VirtKey.f12;\n      case 44:\n        return VirtKey.shift;\n      default:\n        return VirtKey.esc;\n    }\n  }\n\n  @override\n  void write(BinaryWriter writer, VirtKey obj) {\n    switch (obj) {\n      case VirtKey.esc:\n        writer.writeByte(0);\n      case VirtKey.alt:\n        writer.writeByte(1);\n      case VirtKey.home:\n        writer.writeByte(2);\n      case VirtKey.up:\n        writer.writeByte(3);\n      case VirtKey.end:\n        writer.writeByte(4);\n      case VirtKey.sftp:\n        writer.writeByte(5);\n      case VirtKey.snippet:\n        writer.writeByte(6);\n      case VirtKey.tab:\n        writer.writeByte(7);\n      case VirtKey.ctrl:\n        writer.writeByte(8);\n      case VirtKey.left:\n        writer.writeByte(9);\n      case VirtKey.down:\n        writer.writeByte(10);\n      case VirtKey.right:\n        writer.writeByte(11);\n      case VirtKey.clipboard:\n        writer.writeByte(12);\n      case VirtKey.ime:\n        writer.writeByte(13);\n      case VirtKey.pgup:\n        writer.writeByte(14);\n      case VirtKey.pgdn:\n        writer.writeByte(15);\n      case VirtKey.slash:\n        writer.writeByte(16);\n      case VirtKey.backSlash:\n        writer.writeByte(17);\n      case VirtKey.underscore:\n        writer.writeByte(18);\n      case VirtKey.plus:\n        writer.writeByte(19);\n      case VirtKey.equal:\n        writer.writeByte(20);\n      case VirtKey.minus:\n        writer.writeByte(21);\n      case VirtKey.parenLeft:\n        writer.writeByte(22);\n      case VirtKey.parenRight:\n        writer.writeByte(23);\n      case VirtKey.bracketLeft:\n        writer.writeByte(24);\n      case VirtKey.bracketRight:\n        writer.writeByte(25);\n      case VirtKey.braceLeft:\n        writer.writeByte(26);\n      case VirtKey.braceRight:\n        writer.writeByte(27);\n      case VirtKey.chevronLeft:\n        writer.writeByte(28);\n      case VirtKey.chevronRight:\n        writer.writeByte(29);\n      case VirtKey.colon:\n        writer.writeByte(30);\n      case VirtKey.semicolon:\n        writer.writeByte(31);\n      case VirtKey.f1:\n        writer.writeByte(32);\n      case VirtKey.f2:\n        writer.writeByte(33);\n      case VirtKey.f3:\n        writer.writeByte(34);\n      case VirtKey.f4:\n        writer.writeByte(35);\n      case VirtKey.f5:\n        writer.writeByte(36);\n      case VirtKey.f6:\n        writer.writeByte(37);\n      case VirtKey.f7:\n        writer.writeByte(38);\n      case VirtKey.f8:\n        writer.writeByte(39);\n      case VirtKey.f9:\n        writer.writeByte(40);\n      case VirtKey.f10:\n        writer.writeByte(41);\n      case VirtKey.f11:\n        writer.writeByte(42);\n      case VirtKey.f12:\n        writer.writeByte(43);\n      case VirtKey.shift:\n        writer.writeByte(44);\n    }\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is VirtKeyAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass NetViewTypeAdapter extends TypeAdapter<NetViewType> {\n  @override\n  final typeId = 5;\n\n  @override\n  NetViewType read(BinaryReader reader) {\n    switch (reader.readByte()) {\n      case 0:\n        return NetViewType.conn;\n      case 1:\n        return NetViewType.speed;\n      case 2:\n        return NetViewType.traffic;\n      default:\n        return NetViewType.conn;\n    }\n  }\n\n  @override\n  void write(BinaryWriter writer, NetViewType obj) {\n    switch (obj) {\n      case NetViewType.conn:\n        writer.writeByte(0);\n      case NetViewType.speed:\n        writer.writeByte(1);\n      case NetViewType.traffic:\n        writer.writeByte(2);\n    }\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is NetViewTypeAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {\n  @override\n  final typeId = 6;\n\n  @override\n  ServerFuncBtn read(BinaryReader reader) {\n    switch (reader.readByte()) {\n      case 0:\n        return ServerFuncBtn.terminal;\n      case 1:\n        return ServerFuncBtn.sftp;\n      case 2:\n        return ServerFuncBtn.container;\n      case 3:\n        return ServerFuncBtn.process;\n      case 5:\n        return ServerFuncBtn.snippet;\n      case 6:\n        return ServerFuncBtn.iperf;\n      case 8:\n        return ServerFuncBtn.systemd;\n      default:\n        return ServerFuncBtn.terminal;\n    }\n  }\n\n  @override\n  void write(BinaryWriter writer, ServerFuncBtn obj) {\n    switch (obj) {\n      case ServerFuncBtn.terminal:\n        writer.writeByte(0);\n      case ServerFuncBtn.sftp:\n        writer.writeByte(1);\n      case ServerFuncBtn.container:\n        writer.writeByte(2);\n      case ServerFuncBtn.process:\n        writer.writeByte(3);\n      case ServerFuncBtn.snippet:\n        writer.writeByte(5);\n      case ServerFuncBtn.iperf:\n        writer.writeByte(6);\n      case ServerFuncBtn.systemd:\n        writer.writeByte(8);\n    }\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is ServerFuncBtnAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass ServerCustomAdapter extends TypeAdapter<ServerCustom> {\n  @override\n  final typeId = 7;\n\n  @override\n  ServerCustom read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return ServerCustom(\n      pveAddr: fields[1] as String?,\n      pveIgnoreCert: fields[2] == null ? false : fields[2] as bool,\n      cmds: (fields[3] as Map?)?.cast<String, String>(),\n      preferTempDev: fields[4] as String?,\n      logoUrl: fields[5] as String?,\n      netDev: fields[6] as String?,\n      scriptDir: fields[7] as String?,\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, ServerCustom obj) {\n    writer\n      ..writeByte(7)\n      ..writeByte(1)\n      ..write(obj.pveAddr)\n      ..writeByte(2)\n      ..write(obj.pveIgnoreCert)\n      ..writeByte(3)\n      ..write(obj.cmds)\n      ..writeByte(4)\n      ..write(obj.preferTempDev)\n      ..writeByte(5)\n      ..write(obj.logoUrl)\n      ..writeByte(6)\n      ..write(obj.netDev)\n      ..writeByte(7)\n      ..write(obj.scriptDir);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is ServerCustomAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass WakeOnLanCfgAdapter extends TypeAdapter<WakeOnLanCfg> {\n  @override\n  final typeId = 8;\n\n  @override\n  WakeOnLanCfg read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return WakeOnLanCfg(\n      mac: fields[0] as String,\n      ip: fields[1] as String,\n      pwd: fields[2] as String?,\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, WakeOnLanCfg obj) {\n    writer\n      ..writeByte(3)\n      ..writeByte(0)\n      ..write(obj.mac)\n      ..writeByte(1)\n      ..write(obj.ip)\n      ..writeByte(2)\n      ..write(obj.pwd);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is WakeOnLanCfgAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass SystemTypeAdapter extends TypeAdapter<SystemType> {\n  @override\n  final typeId = 9;\n\n  @override\n  SystemType read(BinaryReader reader) {\n    switch (reader.readByte()) {\n      case 0:\n        return SystemType.linux;\n      case 1:\n        return SystemType.bsd;\n      case 2:\n        return SystemType.windows;\n      default:\n        return SystemType.linux;\n    }\n  }\n\n  @override\n  void write(BinaryWriter writer, SystemType obj) {\n    switch (obj) {\n      case SystemType.linux:\n        writer.writeByte(0);\n      case SystemType.bsd:\n        writer.writeByte(1);\n      case SystemType.windows:\n        writer.writeByte(2);\n    }\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is SystemTypeAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n"
  },
  {
    "path": "lib/hive/hive_adapters.g.yaml",
    "content": "# Generated by Hive CE\n# Manual modifications may be necessary for certain migrations\n# Check in to version control\nnextTypeId: 10\ntypes:\n  PrivateKeyInfo:\n    typeId: 1\n    nextIndex: 2\n    fields:\n      id:\n        index: 0\n      key:\n        index: 1\n  Snippet:\n    typeId: 2\n    nextIndex: 5\n    fields:\n      name:\n        index: 0\n      script:\n        index: 1\n      tags:\n        index: 2\n      note:\n        index: 3\n      autoRunOn:\n        index: 4\n  Spi:\n    typeId: 3\n    nextIndex: 16\n    fields:\n      name:\n        index: 0\n      ip:\n        index: 1\n      port:\n        index: 2\n      user:\n        index: 3\n      pwd:\n        index: 4\n      keyId:\n        index: 5\n      tags:\n        index: 6\n      alterUrl:\n        index: 7\n      autoConnect:\n        index: 8\n      jumpId:\n        index: 9\n      custom:\n        index: 10\n      wolCfg:\n        index: 11\n      envs:\n        index: 12\n      id:\n        index: 13\n      customSystemType:\n        index: 14\n      disabledCmdTypes:\n        index: 15\n  VirtKey:\n    typeId: 4\n    nextIndex: 45\n    fields:\n      esc:\n        index: 0\n      alt:\n        index: 1\n      home:\n        index: 2\n      up:\n        index: 3\n      end:\n        index: 4\n      sftp:\n        index: 5\n      snippet:\n        index: 6\n      tab:\n        index: 7\n      ctrl:\n        index: 8\n      left:\n        index: 9\n      down:\n        index: 10\n      right:\n        index: 11\n      clipboard:\n        index: 12\n      ime:\n        index: 13\n      pgup:\n        index: 14\n      pgdn:\n        index: 15\n      slash:\n        index: 16\n      backSlash:\n        index: 17\n      underscore:\n        index: 18\n      plus:\n        index: 19\n      equal:\n        index: 20\n      minus:\n        index: 21\n      parenLeft:\n        index: 22\n      parenRight:\n        index: 23\n      bracketLeft:\n        index: 24\n      bracketRight:\n        index: 25\n      braceLeft:\n        index: 26\n      braceRight:\n        index: 27\n      chevronLeft:\n        index: 28\n      chevronRight:\n        index: 29\n      colon:\n        index: 30\n      semicolon:\n        index: 31\n      f1:\n        index: 32\n      f2:\n        index: 33\n      f3:\n        index: 34\n      f4:\n        index: 35\n      f5:\n        index: 36\n      f6:\n        index: 37\n      f7:\n        index: 38\n      f8:\n        index: 39\n      f9:\n        index: 40\n      f10:\n        index: 41\n      f11:\n        index: 42\n      f12:\n        index: 43\n      shift:\n        index: 44\n  NetViewType:\n    typeId: 5\n    nextIndex: 3\n    fields:\n      conn:\n        index: 0\n      speed:\n        index: 1\n      traffic:\n        index: 2\n  ServerFuncBtn:\n    typeId: 6\n    nextIndex: 9\n    fields:\n      terminal:\n        index: 0\n      sftp:\n        index: 1\n      container:\n        index: 2\n      process:\n        index: 3\n      snippet:\n        index: 5\n      iperf:\n        index: 6\n      systemd:\n        index: 8\n  ServerCustom:\n    typeId: 7\n    nextIndex: 8\n    fields:\n      pveAddr:\n        index: 1\n      pveIgnoreCert:\n        index: 2\n      cmds:\n        index: 3\n      preferTempDev:\n        index: 4\n      logoUrl:\n        index: 5\n      netDev:\n        index: 6\n      scriptDir:\n        index: 7\n  WakeOnLanCfg:\n    typeId: 8\n    nextIndex: 3\n    fields:\n      mac:\n        index: 0\n      ip:\n        index: 1\n      pwd:\n        index: 2\n  SystemType:\n    typeId: 9\n    nextIndex: 3\n    fields:\n      linux:\n        index: 0\n      bsd:\n        index: 1\n      windows:\n        index: 2\n"
  },
  {
    "path": "lib/hive/hive_registrar.g.dart",
    "content": "// Generated by Hive CE\n// Do not modify\n// Check in to version control\n\nimport 'package:hive_ce/hive.dart';\nimport 'package:server_box/data/model/app/tab.dart';\nimport 'package:server_box/data/model/server/connection_stat.dart';\nimport 'package:server_box/hive/hive_adapters.dart';\n\nextension HiveRegistrar on HiveInterface {\n  void registerAdapters() {\n    registerAdapter(AppTabAdapter());\n    registerAdapter(ConnectionResultAdapter());\n    registerAdapter(ConnectionStatAdapter());\n    registerAdapter(NetViewTypeAdapter());\n    registerAdapter(PrivateKeyInfoAdapter());\n    registerAdapter(ServerConnectionStatsAdapter());\n    registerAdapter(ServerCustomAdapter());\n    registerAdapter(ServerFuncBtnAdapter());\n    registerAdapter(SnippetAdapter());\n    registerAdapter(SpiAdapter());\n    registerAdapter(SystemTypeAdapter());\n    registerAdapter(VirtKeyAdapter());\n    registerAdapter(WakeOnLanCfgAdapter());\n  }\n}\n\nextension IsolatedHiveRegistrar on IsolatedHiveInterface {\n  void registerAdapters() {\n    registerAdapter(AppTabAdapter());\n    registerAdapter(ConnectionResultAdapter());\n    registerAdapter(ConnectionStatAdapter());\n    registerAdapter(NetViewTypeAdapter());\n    registerAdapter(PrivateKeyInfoAdapter());\n    registerAdapter(ServerConnectionStatsAdapter());\n    registerAdapter(ServerCustomAdapter());\n    registerAdapter(ServerFuncBtnAdapter());\n    registerAdapter(SnippetAdapter());\n    registerAdapter(SpiAdapter());\n    registerAdapter(SystemTypeAdapter());\n    registerAdapter(VirtKeyAdapter());\n    registerAdapter(WakeOnLanCfgAdapter());\n  }\n}\n"
  },
  {
    "path": "lib/intro.dart",
    "content": "part of 'app.dart';\n\nfinal class _IntroPage extends StatelessWidget {\n  final List<IntroPageBuilder> pages;\n\n  const _IntroPage(this.pages);\n\n  static const _builders = {1: _buildAppSettings, 2: _buildBackupPasswordMigration};\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (context, cons) {\n        final padTop = cons.maxHeight * .16;\n        final pages_ = pages.map((e) => e(context, padTop)).toList();\n        return IntroPage(\n          args: IntroPageArgs(\n            pages: pages_,\n            onDone: (ctx) {\n              Stores.setting.introVer.put(BuildData.build);\n              Navigator.of(ctx).pushReplacement(MaterialPageRoute(builder: (_) => const HomePage()));\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  static Widget _buildAppSettings(BuildContext ctx, double padTop) {\n    return ListView(\n      padding: _introListPad,\n      children: [\n        SizedBox(height: padTop),\n        IntroPage.title(text: libL10n.init, big: true),\n        SizedBox(height: padTop),\n        ListTile(\n          leading: const Icon(IonIcons.language),\n          title: Text(libL10n.language),\n          onTap: () async {\n            final selected = await ctx.showPickSingleDialog(\n              title: libL10n.language,\n              items: AppLocalizations.supportedLocales,\n              display: (p0) => p0.nativeName,\n              initial: _setting.locale.fetch().toLocale,\n            );\n            if (selected != null) {\n              _setting.locale.put(selected.code);\n              RNodes.app.notify();\n            }\n          },\n          trailing: Text(ctx.localeNativeName, style: const TextStyle(fontSize: 15, color: Colors.grey)),\n        ).cardx,\n        ListTile(\n          leading: const Icon(Icons.update),\n          title: Text(libL10n.checkUpdate),\n          subtitle: isAndroid ? Text(l10n.fdroidReleaseTip, style: UIs.textGrey) : null,\n          trailing: StoreSwitch(prop: _setting.autoCheckAppUpdate),\n        ).cardx,\n        ListTile(\n          leading: const Icon(MingCute.delete_2_fill),\n          title: TipText('rm -r', l10n.sftpRmrDirSummary),\n          trailing: StoreSwitch(prop: _setting.sftpRmrDir),\n        ).cardx,\n        ListTile(\n          leading: const Icon(MingCute.chart_line_line, size: _kIconSize),\n          title: TipText('Docker ${l10n.stat}', l10n.parseContainerStatsTip),\n          trailing: StoreSwitch(prop: _setting.containerParseStat),\n        ).cardx,\n        ListTile(\n          leading: const Icon(Bootstrap.alphabet),\n          title: TipText(l10n.letterCache, l10n.letterCacheTip),\n          trailing: StoreSwitch(prop: _setting.letterCache),\n        ).cardx,\n        UIs.height77,\n      ],\n    );\n  }\n\n  static Widget _buildBackupPasswordMigration(BuildContext ctx, double padTop) {\n    return ListView(\n      padding: _introListPad,\n      children: [\n        SizedBox(height: padTop),\n        IntroPage.title(text: l10n.backupPassword, big: true),\n        SizedBox(height: padTop * 0.5),\n        Text(\n          l10n.backupTip,\n          style: const TextStyle(fontSize: 16),\n          textAlign: TextAlign.center,\n        ),\n        SizedBox(height: padTop * 0.5),\n        ListTile(\n          leading: const Icon(Icons.lock, color: Colors.orange),\n          title: Text(l10n.backupPassword),\n          subtitle: Text(l10n.backupPasswordTip, style: UIs.textGrey),\n          trailing: const Icon(Icons.keyboard_arrow_right),\n          onTap: () async {\n            final controller = TextEditingController();\n            final result = await ctx.showRoundDialog<bool>(\n              title: l10n.backupPassword,\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  Text(l10n.backupPasswordTip, style: UIs.textGrey),\n                  UIs.height13,\n                  Input(\n                    label: l10n.backupPassword,\n                    controller: controller,\n                    obscureText: true,\n                    onSubmitted: (_) => ctx.pop(true),\n                  ),\n                ],\n              ),\n              actions: Btnx.cancelOk,\n            );\n            if (result == true) {\n              final pwd = controller.text.trim();\n              if (pwd.isNotEmpty) {\n                await SecureStoreProps.bakPwd.write(pwd);\n                ctx.showSnackBar(l10n.backupPasswordSet);\n              }\n            }\n          },\n        ).cardx,\n        SizedBox(height: padTop),\n        Text(\n          'This step is recommended for secure backup functionality.',\n          style: UIs.textGrey,\n          textAlign: TextAlign.center,\n        ),\n        UIs.height77,\n      ],\n    );\n  }\n\n  static Future<List<IntroPageBuilder>> get builders async {\n    final storedVer = _setting.introVer.fetch();\n    final lastVer = _setting.lastVer.fetch();\n\n    // If user is upgrading from older version and doesn't have backup password set,\n    // show the backup password migration page\n    final hasBackupPwd = (await SecureStoreProps.bakPwd.read())?.isNotEmpty == true;\n    final isUpgrading = lastVer > 0 && storedVer < 2; // lastVer > 0 means not first install\n\n    final builders = _builders.entries\n        .where((e) {\n          if (e.key == 2 && (!isUpgrading || hasBackupPwd)) {\n            return false; // Skip backup password migration if not upgrading or already has password\n          }\n          return e.key > storedVer;\n        })\n        .map((e) => e.value)\n        .toList();\n\n    return builders;\n  }\n\n  static final _setting = Stores.setting;\n  static const _kIconSize = 23.0;\n  static const _introListPad = EdgeInsets.symmetric(horizontal: 17);\n}\n"
  },
  {
    "path": "lib/l10n/app_de.arb",
    "content": "{\n  \"@@locale\": \"de\",\n  \"aboutThanks\": \"Vielen Dank an die folgenden Personen, die daran teilgenommen haben.\\n\",\n  \"acceptBeta\": \"Akzeptieren Sie Testversion-Updates\",\n  \"addSystemPrivateKeyTip\": \"Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?\",\n  \"added2List\": \"Zur Aufgabenliste hinzugefügt\",\n  \"addr\": \"Adresse\",\n  \"alreadyLastDir\": \"Bereits im letzten Verzeichnis.\",\n  \"askAi\": \"KI fragen\",\n  \"askAiApiKey\": \"API-Schlüssel\",\n  \"askAiAwaitingResponse\": \"Warte auf KI-Antwort...\",\n  \"askAiBaseUrl\": \"Basis-URL\",\n  \"askAiCommandInserted\": \"Befehl ins Terminal eingefügt\",\n  \"askAiConfigMissing\": \"Bitte konfigurieren Sie {fields} in den Einstellungen.\",\n  \"askAiConfirmExecute\": \"Vor Ausführung bestätigen\",\n  \"askAiConversation\": \"KI-Unterhaltung\",\n  \"askAiDisclaimer\": \"KI kann Fehler machen. Bitte vorsichtig verwenden.\",\n  \"askAiFollowUpHint\": \"Weitere Frage stellen...\",\n  \"askAiInsertTerminal\": \"In Terminal einfügen\",\n  \"askAiNoResponse\": \"Keine Antwort\",\n  \"askAiRecommendedCommand\": \"KI-empfohlener Befehl\",\n  \"askAiSelectedContent\": \"Ausgewählter Inhalt\",\n  \"askAiUsageHint\": \"Verwendet im SSH-Terminal\",\n  \"atLeastOneTab\": \"Mindestens ein Tab muss ausgewählt sein\",\n  \"authFailTip\": \"Authentifizierung fehlgeschlagen, bitte überprüfen Sie, ob das Passwort/Schlüssel/Host/Benutzer usw. falsch sind.\",\n  \"autoBackupConflict\": \"Es kann nur eine automatische Sicherung gleichzeitig aktiviert werden.\",\n  \"autoConnect\": \"Automatisch verbinden\",\n  \"autoRun\": \"Automatischer Start\",\n  \"autoUpdateHomeWidget\": \"Home-Widget automatisch aktualisieren\",\n  \"availableTabs\": \"Verfügbare Tabs\",\n  \"backupEncrypted\": \"Backup ist verschlüsselt\",\n  \"backupNotEncrypted\": \"Backup ist nicht verschlüsselt\",\n  \"backupPassword\": \"Backup-Passwort\",\n  \"backupPasswordRemoved\": \"Backup-Passwort entfernt\",\n  \"backupPasswordSet\": \"Backup-Passwort gesetzt\",\n  \"backupPasswordTip\": \"Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.\",\n  \"backupPasswordWrong\": \"Falsches Backup-Passwort\",\n  \"backupTip\": \"Die exportierten Daten können mit einem Passwort verschlüsselt werden. \\nBitte sicher aufbewahren.\",\n  \"backupVersionNotMatch\": \"Die Backup-Version stimmt nicht überein.\",\n  \"bgRun\": \"Hintergrundaktualisierung\",\n  \"bgRunTip\": \"Dieser Schalter bedeutet nur, dass die App versuchen wird, im Hintergrund zu laufen. Ob sie im Hintergrund laufen kann, hängt davon ab, ob die Berechtigungen aktiviert sind oder nicht. Bei nativem Android deaktivieren Sie bitte \\\"Batterieoptimierung\\\" in dieser App, und bei miui ändern Sie bitte die Energiesparrichtlinie auf \\\"Unbegrenzt\\\".\",\n  \"clearAllStatsContent\": \"Sind Sie sicher, dass Sie alle Server-Verbindungsstatistiken löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.\",\n  \"clearAllStatsTitle\": \"Alle Statistiken löschen\",\n  \"clearServerStatsContent\": \"Sind Sie sicher, dass Sie die Verbindungsstatistiken für Server \\\"{serverName}\\\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.\",\n  \"clearServerStatsTitle\": \"{serverName} Statistiken löschen\",\n  \"clearThisServerStats\": \"Statistiken dieses Servers löschen\",\n  \"compactDatabase\": \"Datenbank komprimieren\",\n  \"compactDatabaseContent\": \"Datenbankgröße: {size}\\n\\nDies wird die Datenbank neu organisieren, um die Dateigröße zu reduzieren. Es werden keine Daten gelöscht.\",\n  \"closeAfterSave\": \"Speichern und schließen\",\n  \"collapseUITip\": \"Ob lange Listen in der Benutzeroberfläche standardmäßig eingeklappt werden sollen oder nicht\",\n  \"connectionDetails\": \"Verbindungsdetails\",\n  \"connectionStats\": \"Verbindungsstatistiken\",\n  \"connectionStatsDesc\": \"Server-Verbindungserfolgsrate und Verlauf anzeigen\",\n  \"containerTrySudoTip\": \"Zum Beispiel: In der App ist der Benutzer auf aaa eingestellt, aber Docker ist unter dem Root-Benutzer installiert. In diesem Fall müssen Sie diese Option aktivieren\",\n  \"containerSudoPasswordRequired\": \"Ein sudo-Passwort ist erforderlich, um auf Docker zuzugreifen. Bitte geben Sie Ihr Passwort ein.\",\n  \"containerSudoPasswordIncorrect\": \"Das sudo-Passwort ist falsch oder nicht erlaubt. Bitte versuchen Sie es erneut.\",\n  \"convert\": \"Konvertieren\",\n  \"copyPath\": \"Pfad kopieren\",\n  \"cpuViewAsProgressTip\": \"Zeigen Sie die Auslastung jedes CPUs in einem Fortschrittsbalken-Stil an (alter Stil)\",\n  \"cursorType\": \"Cursor-Typ\",\n  \"customCmd\": \"Benutzerdefinierte Befehle\",\n  \"customCmdHint\": \"\\\"Befehlsname\\\": \\\"Befehl\\\"\",\n  \"deleteServers\": \"Batch-Löschung von Servern\",\n  \"desktopTerminalTip\": \"Befehl zum Öffnen des Terminal-Emulators beim Starten von SSH-Sitzungen.\",\n  \"dirEmpty\": \"Stelle sicher, dass der Ordner leer ist.\",\n  \"discoverSshServers\": \"SSH-Server entdecken\",\n  \"discoveryFailed\": \"Entdeckung fehlgeschlagen\",\n  \"discoverySettings\": \"Entdeckungseinstellungen\",\n  \"discoverySummary\": \"Entdeckungs-Zusammenfassung\",\n  \"diskHealth\": \"Festplattengesundheit\",\n  \"diskIgnorePath\": \"Pfad für Datenträger ignorieren\",\n  \"displayCpuIndex\": \"Zeigen Sie den CPU-Index an\",\n  \"dl2Local\": \"Datei \\\"{fileName}\\\" herunterladen?\",\n  \"dockerEmptyRunningItems\": \"Es gibt keine laufenden Container.\\nDas könnte daran liegen:\\n- Der Docker-Installationsbenutzer ist nicht mit dem in der App konfigurierten Benutzernamen identisch.\\n- Die Umgebungsvariable DOCKER_HOST wurde nicht korrekt gelesen. Sie können sie ermitteln, indem Sie `echo $DOCKER_HOST` im Terminal ausführen.\",\n  \"dockerImagesFmt\": \"{count} Image(s)\",\n  \"dockerNotInstalled\": \"Docker ist nicht installiert\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} aktiv, {stoppedCount} container gestoppt.\",\n  \"dockerStatusRunningFmt\": \"{count} Container aktiv\",\n  \"doubleColumnMode\": \"Doppelspaltiger Modus\",\n  \"doubleColumnTip\": \"Diese Option aktiviert nur die Funktion, ob sie tatsächlich aktiviert werden kann, hängt auch von der Breite des Geräts ab\",\n  \"editVirtKeys\": \"Virtuelle Tasten bearbeiten\",\n  \"editorHighlightTip\": \"Die Leistung der aktuellen Codehervorhebung ist schlechter und kann zur Verbesserung optional ausgeschaltet werden.\",\n  \"enableMdns\": \"mDNS aktivieren\",\n  \"enableMdnsDesc\": \"mDNS/Bonjour verwenden, um SSH-Dienste zu entdecken\",\n  \"envVars\": \"Umgebungsvariable\",\n  \"experimentalFeature\": \"Experimentelles Feature\",\n  \"extraArgs\": \"Extra args\",\n  \"fallbackSshDest\": \"SSH-Fallback-Ziel\",\n  \"fdroidReleaseTip\": \"Wenn Sie diese App von F-Droid heruntergeladen haben, wird empfohlen, diese Option zu deaktivieren.\",\n  \"fgService\": \"Vordergrund-Dienst\",\n  \"fgServiceTip\": \"Nach dem Einschalten kann es bei einigen Gerätemodellen zu Abstürzen kommen. Das Ausschalten kann bei einigen Modellen dazu führen, dass SSH-Verbindungen im Hintergrund nicht aufrechterhalten werden können. Bitte erlauben Sie ServerBox in den Systemeinstellungen Benachrichtigungsrechte, Hintergrundausführung und Selbstaktivierung.\",\n  \"fileTooLarge\": \"Datei '{file}' ist zu groß {size}, max {sizeMax}\",\n  \"finishedAt\": \"Beendet um\",\n  \"followSystem\": \"System verfolgen\",\n  \"fontSize\": \"Schriftgröße\",\n  \"fullScreen\": \"Vollbildmodus\",\n  \"fullScreenJitter\": \"Jitter im Vollbildmodus\",\n  \"fullScreenJitterHelp\": \"Einbrennen des Bildschirms verhindern\",\n  \"fullScreenTip\": \"Soll der Vollbildmodus aktiviert werden, wenn das Gerät in den Quermodus gedreht wird? Diese Option gilt nur für die Server-Registerkarte.\",\n  \"goBackQ\": \"Zurückkommen?\",\n  \"goto\": \"Pfad öffnen\",\n  \"hideTitleBar\": \"Titelleiste ausblenden\",\n  \"highlight\": \"Code highlight\",\n  \"homeTabs\": \"Home-Tabs\",\n  \"homeTabsCustomizeDesc\": \"Passen Sie an, welche Tabs auf der Startseite angezeigt werden und ihre Reihenfolge\",\n  \"homeWidgetUrlConfig\": \"Home-Widget-Link konfigurieren\",\n  \"httpFailedWithCode\": \"Anfrage fehlgeschlagen, Statuscode: {code}\",\n  \"ignoreCert\": \"Zertifikat ignorieren\",\n  \"image\": \"Image\",\n  \"imagesList\": \"Images\",\n  \"installDockerWithUrl\": \"Bitte installiere docker zuerst. https://docs.docker.com/engine/install\",\n  \"invalid\": \"Ungültig\",\n  \"jumpServer\": \"Server springen\",\n  \"keepForeground\": \"Stelle sicher, dass die App geöffnet bleibt.\",\n  \"keepStatusWhenErr\": \"Den letzten Serverstatus beibehalten\",\n  \"keepStatusWhenErrTip\": \"Nur im Fehlerfall während der Ausführung des Skripts\",\n  \"keyAuth\": \"Schlüsselauthentifzierung\",\n  \"lastFailure\": \"Letzter Fehler\",\n  \"lastSuccess\": \"Letzter Erfolg\",\n  \"letterCache\": \"Buchstaben-Caching\",\n  \"letterCacheTip\": \"Empfohlen, zu deaktivieren, aber nach dem Deaktivieren können keine CJK-Zeichen eingegeben werden.\",\n  \"madeWithLove\": \"Erstellt mit ❤️ von {myGithub}\",\n  \"max\": \"max\",\n  \"maxConcurrency\": \"Maximale Gleichzeitigkeit\",\n  \"maxRetryCount\": \"Anzahl an Verbindungsversuchen\",\n  \"maxRetryCountEqual0\": \"Unbegrenzte Verbindungsversuche zum Server\",\n  \"min\": \"min\",\n  \"more\": \"Mehr\",\n  \"moveOutServerFuncBtnsHelp\": \"Ein: kann unter jeder Karte auf der Registerkarte \\\"Server\\\" angezeigt werden. Aus: kann oben auf der Seite \\\"Serverdetails\\\" angezeigt werden.\",\n  \"needHomeDir\": \"Wenn Sie ein Synology-Benutzer sind, [sehen Sie hier](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Benutzer anderer Systeme müssen suchen, wie man ein Home-Verzeichnis erstellt.\",\n  \"needRestart\": \"App muss neugestartet werden\",\n  \"netViewType\": \"Netzwerkansicht Typ\",\n  \"newContainer\": \"Neuer Container\",\n  \"noConnectionStatsData\": \"Keine Verbindungsstatistikdaten\",\n  \"noLineChart\": \"Verwenden Sie keine Liniendiagramme\",\n  \"noLineChartForCpu\": \"Verwenden Sie keine Liniendiagramme für CPU\",\n  \"noPrivateKeyTip\": \"Der private Schlüssel existiert nicht, möglicherweise wurde er gelöscht oder es liegt ein Konfigurationsfehler vor.\",\n  \"noPromptAgain\": \"Nicht mehr nachfragen\",\n  \"onServerDetailPage\": \"in Detailansicht des Servers\",\n  \"onlyOneLine\": \"Nur als eine Zeile anzeigen (scrollbar)\",\n  \"onlyWhenCoreBiggerThan8\": \"Wirksam nur, wenn die Anzahl der Kerne > 8 ist.\",\n  \"openLastPath\": \"Öffnen Sie den letzten Pfad\",\n  \"openLastPathTip\": \"Verschiedene Server haben unterschiedliche Einträge, und der Eintrag ist der Pfad zum Ausgang\",\n  \"parseContainerStatsTip\": \"Das Analysieren des Belegungsstatus durch Docker ist relativ langsam\",\n  \"percentOfSize\": \"{percent}% von {size}\",\n  \"permission\": \"Berechtigungen\",\n  \"pingInputIP\": \"Bitte gib eine Ziel-IP/Domain ein.\",\n  \"pingNoServer\": \"Kein Server zum Anpingen.\\nBitte füge einen Server hinzu.\",\n  \"plugInType\": \"Einfügetyp\",\n  \"preferDiskAmount\": \"Festplattenkapazität vorrangig anzeigen\",\n  \"privateKey\": \"Private Key\",\n  \"privateKeyNotFoundFmt\": \"Privater Schlüssel [{keyId}] wurde nicht gefunden.\",\n  \"pushToken\": \"Push Token\",\n  \"pveIgnoreCertTip\": \"Nicht empfohlen, Achten Sie auf Sicherheitsrisiken! Wenn Sie das Standardzertifikat von PVE verwenden, müssen Sie diese Option aktivieren.\",\n  \"pveLoginFailed\": \"Anmeldung fehlgeschlagen. Kann nicht mit Benutzername/Passwort aus der Serverkonfiguration angemeldet werden, um sich über Linux PAM anzumelden.\",\n  \"pveVersionLow\": \"Diese Funktion befindet sich derzeit in der Testphase und wurde nur auf PVE 8+ getestet. Bitte verwenden Sie sie mit Vorsicht.\",\n  \"read\": \"Lesen\",\n  \"recentConnections\": \"Kürzliche Verbindungen\",\n  \"rememberPwdInMem\": \"Passwort im Speicher behalten\",\n  \"rememberPwdInMemTip\": \"Für Container, Aufhängen usw.\",\n  \"rememberWindowSize\": \"Fenstergröße merken\",\n  \"remotePath\": \"Entfernte Pfade\",\n  \"result\": \"Result\",\n  \"rotateAngel\": \"Rotationswinkel\",\n  \"sameIdServerExist\": \"Ein Server mit derselben ID existiert bereits\",\n  \"save\": \"Speichern\",\n  \"second\": \"s\",\n  \"serverDetailOrder\": \"Reihenfolge der Widgets auf der Detailseite\",\n  \"serverFuncBtns\": \"Server-Funktionsschaltflächen\",\n  \"serverOrder\": \"Server-Bestellung\",\n  \"serverTabRequired\": \"Server-Tab kann nicht entfernt werden\",\n  \"sftpDlPrepare\": \"Verbindung vorbereiten...\",\n  \"sftpEditorTip\": \"Wenn leer, verwenden Sie den im App integrierten Dateieditor. Wenn ein Wert vorhanden ist, wird der Editor des Remote-Servers verwendet, z.B. `vim` (es wird empfohlen, automatisch gemäß `EDITOR` zu ermitteln).\",\n  \"sftpRmrDirSummary\": \"Verwenden Sie \\\"rm -r\\\", um das Verzeichnis in SFTP zu löschen.\",\n  \"sftpSSHConnected\": \"SFTP Verbunden\",\n  \"sftpShowFoldersFirst\": \"Ordner zuerst anzeigen\",\n  \"showDistLogo\": \"Distributionslogo anzeigen\",\n  \"size\": \"Größe\",\n  \"softWrap\": \"Weicher Umbruch\",\n  \"specifyDev\": \"Gerät angeben\",\n  \"specifyDevTip\": \"Zum Beispiel bezieht sich die Standard-Netzwerkverkehrsstatistik auf alle Geräte. Hier können Sie ein bestimmtes Gerät angeben.\",\n  \"speed\": \"Tempo\",\n  \"spentTime\": \"Benötigte Zeit: {time}\",\n  \"sshConfigAllExist\": \"Alle Server existieren bereits ({duplicateCount} Duplikate gefunden)\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} Duplikate werden übersprungen\",\n  \"sshConfigFound\": \"Wir haben SSH-Konfiguration auf Ihrem System gefunden.\",\n  \"sshConfigFoundServers\": \"{totalCount} Server gefunden\",\n  \"sshConfigImport\": \"SSH-Konfiguration importieren\",\n  \"sshConfigImportHelp\": \"Es können nur Basisinformationen importiert werden, zum Beispiel: IP/Port.\",\n  \"sshConfigImportPermission\": \"Möchten Sie die Berechtigung erteilen, ~/.ssh/config zu lesen und Server-Einstellungen automatisch zu importieren?\",\n  \"sshConfigImportTip\": \"Bei der ersten Server-Erstellung zum Lesen von ~/.ssh/config auffordern\",\n  \"sshConfigImported\": \"{count} Server aus SSH-Konfiguration importiert\",\n  \"sshHostKeyChangedDesc\": \"Der SSH-Hostschlüssel für {serverName} hat sich geändert. Fahren Sie nur fort, wenn Sie diesem Server vertrauen.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Fingerabdruck (MD5 Base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Fingerabdruck (MD5 Hex): {fingerprint}\",\n  \"sshHostKeyType\": \"SSH-Hostschlüsseltyp\",\n  \"sshHostKeyNewDesc\": \"Ein neuer SSH-Hostschlüssel wurde von {serverName} empfangen. Prüfen Sie den Fingerabdruck, bevor Sie vertrauen.\",\n  \"sshHostKeyStoredFingerprint\": \"Gespeicherter Fingerabdruck: {fingerprint}\",\n  \"sshConfigManualSelect\": \"Möchten Sie die SSH-Konfigurationsdatei manuell auswählen?\",\n  \"sshConfigNoServers\": \"Keine Server in der SSH-Konfiguration gefunden\",\n  \"sshConfigPermissionDenied\": \"Aufgrund der macOS-Berechtigungen kann nicht auf die SSH-Konfigurationsdatei zugegriffen werden.\",\n  \"sshConfigServersToImport\": \"{importCount} Server werden importiert\",\n  \"sshTermHelp\": \"Wenn das Terminal scrollbar ist, kann durch horizontales Ziehen Text ausgewählt werden. Durch Klicken auf die Tastentaste wird die Tastatur ein- oder ausgeschaltet. Das Dateisymbol öffnet den aktuellen Pfad SFTP. Die Zwischenablage-Schaltfläche kopiert den Inhalt, wenn Text ausgewählt ist, und fügt Inhalte aus der Zwischenablage in das Terminal ein, wenn kein Text ausgewählt ist und Inhalte in der Zwischenablage vorhanden sind. Das Codesymbol fügt Code-Schnipsel ins Terminal ein und führt sie aus.\",\n  \"sshTip\": \"Diese Funktion befindet sich jetzt in der Experimentierphase.\\n\\nBitte melde Bugs auf {url} oder mach mit bei der Entwicklung.\",\n  \"sshVirtualKeyAutoOff\": \"Automatische Umschaltung der virtuellen Tasten\",\n  \"stat\": \"Statistik\",\n  \"supportFmtArgs\": \"Die folgenden Formatierungsparameter werden unterstützt:\",\n  \"suspendTip\": \"Die Suspend-Funktion erfordert Root-Rechte und systemd-Unterstützung.\",\n  \"switchTo\": \"Wechseln zu {val}\",\n  \"syncTip\": \"Damit einige Änderungen wirksam werden, kann ein Neustart erforderlich sein.\",\n  \"system\": \"Systeme\",\n  \"tag\": \"Tags\",\n  \"tapToStartDiscovery\": \"Tippen Sie auf die Suche-Schaltfläche, um SSH-Server in Ihrem Netzwerk zu entdecken\",\n  \"termFontSizeTip\": \"Diese Einstellung beeinflusst die Größe des Terminals (Breite und Höhe). Sie können die Terminalseite zoomen, um die Schriftgröße der aktuellen Sitzung anzupassen.\",\n  \"textScaler\": \"Skalierung der Schriftart\",\n  \"textScalerTip\": \"1.0 => 100% (Originalgröße), funktioniert nur auf der Serverseite Teil der Schrift, nicht empfohlen zu ändern.\",\n  \"time\": \"Zeit\",\n  \"times\": \"x\",\n  \"trySudo\": \"Versuche es mit sudo\",\n  \"unknown\": \"Unbekannt\",\n  \"unkownConvertMode\": \"Unbekannter Konvertierungsmodus\",\n  \"update\": \"Update\",\n  \"updateIntervalEqual0\": \"Wenn du den Wert 0 einstellst, wird nicht automatisch aktualisiert.\\nDer CPU-Status kann nicht berechnet werden.\",\n  \"updateServerStatusInterval\": \"Aktualisierungsintervall des Serverstatus\",\n  \"upsideDown\": \"Upside Down\",\n  \"useCdn\": \"Verwenden von CDN\",\n  \"useCdnTip\": \"Nicht-chinesischen Benutzern wird die Verwendung eines CDN empfohlen. Möchten Sie es verwenden?\",\n  \"useNoPwd\": \"Es wird kein Passwort verwendet\",\n  \"usePodmanByDefault\": \"Standardmäßige Verwendung von Podman\",\n  \"used\": \"Gebraucht\",\n  \"view\": \"Ansicht\",\n  \"viewDetails\": \"Details anzeigen\",\n  \"viewErr\": \"Fehler anzeigen\",\n  \"virtKeyHelpClipboard\": \"In die Zwischenablage kopieren, wenn das ausgewählte Terminal nicht leer ist, andernfalls den Inhalt der Zwischenablage in das Terminal einfügen.\",\n  \"virtKeyHelpIME\": \"Tastatur ein-/ausschalten\",\n  \"virtKeyHelpSFTP\": \"Aktuelles Verzeichnis in SFTP öffnen.\",\n  \"waitConnection\": \"Bitte warte, bis die Verbindung hergestellt wurde.\",\n  \"wakeLock\": \"Wach halten\",\n  \"watchNotPaired\": \"Keine gekoppelte Apple Watch\",\n  \"webdavSettingEmpty\": \"Webdav-Einstellungen sind leer\",\n  \"whenOpenApp\": \"Beim Öffnen der App\",\n  \"wolTip\": \"Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.\",\n  \"write\": \"Schreiben\",\n  \"writeScriptFailTip\": \"Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.\",\n  \"writeScriptTip\": \"Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \\n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.\",\n  \"podmanDockerEmulationDetected\": \"Podman Docker-Emulation erkannt. Bitte wechseln Sie in den Einstellungen zu Podman.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_en.arb",
    "content": "{\n  \"@@locale\": \"en\",\n  \"aboutThanks\": \"Thanks to the following people who participated in.\",\n  \"acceptBeta\": \"Accept beta version updates\",\n  \"addSystemPrivateKeyTip\": \"Currently private keys don't exist, do you want to add the one that comes with the system (~/.ssh/id_rsa)?\",\n  \"added2List\": \"Added to task list\",\n  \"addr\": \"Address\",\n  \"alreadyLastDir\": \"Already in last directory.\",\n  \"askAi\": \"Ask AI\",\n  \"askAiApiKey\": \"API Key\",\n  \"askAiAwaitingResponse\": \"Waiting for AI response...\",\n  \"askAiBaseUrl\": \"Base URL\",\n  \"askAiCommandInserted\": \"Command inserted into terminal\",\n  \"askAiConfigMissing\": \"Please configure {fields} in Settings.\",\n  \"askAiConfirmExecute\": \"Confirm before executing\",\n  \"askAiConversation\": \"AI conversation\",\n  \"askAiDisclaimer\": \"AI may be incorrect. Review carefully before applying.\",\n  \"askAiFollowUpHint\": \"Ask a follow-up...\",\n  \"askAiInsertTerminal\": \"Insert into terminal\",\n  \"askAiNoResponse\": \"No response\",\n  \"askAiRecommendedCommand\": \"AI suggested command\",\n  \"askAiSelectedContent\": \"Selected content\",\n  \"askAiUsageHint\": \"Used in SSH Terminal\",\n  \"atLeastOneTab\": \"At least one tab must be selected\",\n  \"authFailTip\": \"Authentication failed, please check whether credentials are correct\",\n  \"autoBackupConflict\": \"Only one automatic backup can be turned on at the same time.\",\n  \"autoConnect\": \"Auto connect\",\n  \"autoRun\": \"Auto run\",\n  \"autoUpdateHomeWidget\": \"Automatic home widget update\",\n  \"availableTabs\": \"Available Tabs\",\n  \"backupEncrypted\": \"Backup is encrypted\",\n  \"backupNotEncrypted\": \"Backup is not encrypted\",\n  \"backupPassword\": \"Backup password\",\n  \"backupPasswordRemoved\": \"Backup password removed\",\n  \"backupPasswordSet\": \"Backup password set\",\n  \"backupPasswordTip\": \"Set a password to encrypt backup files. Leave empty to disable encryption.\",\n  \"backupPasswordWrong\": \"Incorrect backup password\",\n  \"backupTip\": \"The exported data can be encrypted with password. \\nPlease keep it safe.\",\n  \"backupVersionNotMatch\": \"Backup version is not match.\",\n  \"bgRun\": \"Run in background\",\n  \"bgRunTip\": \"This switch only means the program will try to run in the background. Whether it can run in the background depends on whether the permission is enabled or not. For AOSP-based Android ROMs, please disable \\\"Battery Optimization\\\" in this app. For MIUI / HyperOS, please change the power saving policy to \\\"Unlimited\\\".\",\n  \"clearAllStatsContent\": \"Are you sure you want to clear all server connection statistics? This action cannot be undone.\",\n  \"clearAllStatsTitle\": \"Clear All Statistics\",\n  \"clearServerStatsContent\": \"Are you sure you want to clear connection statistics for server \\\"{serverName}\\\"? This action cannot be undone.\",\n  \"clearServerStatsTitle\": \"Clear {serverName} Statistics\",\n  \"clearThisServerStats\": \"Clear This Server Statistics\",\n  \"compactDatabase\": \"Compact Database\",\n  \"compactDatabaseContent\": \"Database size: {size}\\n\\nThis will reorganize the database to reduce file size. No data will be deleted.\",\n  \"closeAfterSave\": \"Save and close\",\n  \"collapseUITip\": \"Whether to collapse long lists present in the UI by default\",\n  \"connectionDetails\": \"Connection Details\",\n  \"connectionStats\": \"Connection Statistics\",\n  \"connectionStatsDesc\": \"View server connection success rate and history\",\n  \"containerTrySudoTip\": \"For example: In the app, the user is set to aaa, but Docker is installed under the root user. In this case, you need to enable this option.\",\n  \"containerSudoPasswordRequired\": \"Sudo password is required to access Docker. Please enter your password.\",\n  \"containerSudoPasswordIncorrect\": \"Sudo password is incorrect or not allowed. Please try again.\",\n  \"convert\": \"Convert\",\n  \"copyPath\": \"Copy path\",\n  \"cpuViewAsProgressTip\": \"Display the usage of each CPU in a progress bar style (old style)\",\n  \"cursorType\": \"Cursor type\",\n  \"customCmd\": \"Custom commands\",\n  \"customCmdHint\": \"\\\"Command Name\\\": \\\"Command\\\"\",\n  \"deleteServers\": \"Batch delete servers\",\n  \"desktopTerminalTip\": \"Command used to open the terminal emulator when launching SSH sessions.\",\n  \"dirEmpty\": \"Make sure the folder is empty.\",\n  \"discoverSshServers\": \"Discover SSH Servers\",\n  \"discoveryFailed\": \"Discovery failed\",\n  \"discoverySettings\": \"Discovery Settings\",\n  \"discoverySummary\": \"Discovery Summary\",\n  \"diskHealth\": \"Disk Health\",\n  \"diskIgnorePath\": \"Ignore path for disk\",\n  \"displayCpuIndex\": \"Display CPU index\",\n  \"dl2Local\": \"Download {fileName} to local?\",\n  \"dockerEmptyRunningItems\": \"There are no running containers.\\nThis could be because:\\n- The Docker installation user is not the same as the username configured within the App.\\n- The environment variable DOCKER_HOST was not read correctly. You can get it by running `echo $DOCKER_HOST` in the terminal.\",\n  \"dockerImagesFmt\": \"{count} images\",\n  \"dockerNotInstalled\": \"Docker not installed\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} running, {stoppedCount} container stopped.\",\n  \"dockerStatusRunningFmt\": \"{count} container running.\",\n  \"doubleColumnMode\": \"Double column mode\",\n  \"doubleColumnTip\": \"This option only enables the feature, whether it can actually be enabled depends on the width of the device\",\n  \"editVirtKeys\": \"Edit virtual keys\",\n  \"editorHighlightTip\": \"The current code highlighting performance is not ideal and can be optionally turned off to improve.\",\n  \"enableMdns\": \"Enable mDNS\",\n  \"enableMdnsDesc\": \"Use mDNS/Bonjour to discover SSH services\",\n  \"envVars\": \"Environment variable\",\n  \"experimentalFeature\": \"Experimental feature\",\n  \"extraArgs\": \"Extra arguments\",\n  \"fallbackSshDest\": \"Fallback SSH destination\",\n  \"fdroidReleaseTip\": \"If you downloaded this app from F-Droid, it is recommended to turn off this option.\",\n  \"fgService\": \"Foreground Service\",\n  \"fgServiceTip\": \"After enabling, some device models may crash. Disabling it may cause some models to be unable to maintain SSH connections in the background. Please allow ServerBox notification permissions, background running, and self-wake-up in system settings.\",\n  \"fileTooLarge\": \"File '{file}' too large {size}, max {sizeMax}\",\n  \"finishedAt\": \"Finished at\",\n  \"followSystem\": \"Follow system\",\n  \"fontSize\": \"Font size\",\n  \"fullScreen\": \"Full screen mode\",\n  \"fullScreenJitter\": \"Full screen jitter\",\n  \"fullScreenJitterHelp\": \"To avoid screen burn-in\",\n  \"fullScreenTip\": \"Should full-screen mode be enabled when the device is rotated to landscape mode? This option only applies to the server tab.\",\n  \"goBackQ\": \"Go back?\",\n  \"goto\": \"Go to\",\n  \"hideTitleBar\": \"Hide title bar\",\n  \"highlight\": \"Code highlighting\",\n  \"homeTabs\": \"Home Tabs\",\n  \"homeTabsCustomizeDesc\": \"Customize which tabs appear on the home page and their order\",\n  \"homeWidgetUrlConfig\": \"Config home widget url\",\n  \"httpFailedWithCode\": \"request failed, status code: {code}\",\n  \"ignoreCert\": \"Ignore certificate\",\n  \"image\": \"Image\",\n  \"imagesList\": \"Images list\",\n  \"installDockerWithUrl\": \"Please https://docs.docker.com/engine/install docker first.\",\n  \"invalid\": \"Invalid\",\n  \"invalidHostFormat\": \"Invalid host format. Only IPv4, IPv6, and domain characters are allowed.\",\n  \"jumpServer\": \"Jump server\",\n  \"keepForeground\": \"Keep app foreground!\",\n  \"keepStatusWhenErr\": \"Preserve the last server state\",\n  \"keepStatusWhenErrTip\": \"Only in the event of an error during script execution\",\n  \"keyAuth\": \"Key Auth\",\n  \"lastFailure\": \"Last Failure\",\n  \"lastSuccess\": \"Last Success\",\n  \"letterCache\": \"Letter caching\",\n  \"letterCacheTip\": \"Recommended to disable, but after disabling, it will be impossible to input CJK characters.\",\n  \"madeWithLove\": \"Made with ❤️ by {myGithub}\",\n  \"max\": \"max\",\n  \"maxConcurrency\": \"Max Concurrency\",\n  \"maxRetryCount\": \"Number of server reconnections\",\n  \"maxRetryCountEqual0\": \"Will retry again and again.\",\n  \"min\": \"min\",\n  \"more\": \"More\",\n  \"moveOutServerFuncBtnsHelp\": \"On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.\",\n  \"needHomeDir\": \"If you are a Synology user, [see here](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Users of other systems need to search for how to create a home directory.\",\n  \"needRestart\": \"App needs to be restarted\",\n  \"netViewType\": \"Network view type\",\n  \"newContainer\": \"New container\",\n  \"noConnectionStatsData\": \"No connection statistics data\",\n  \"noLineChart\": \"Do not use line charts\",\n  \"noLineChartForCpu\": \"Do not use line charts for CPU\",\n  \"noPrivateKeyTip\": \"The private key does not exist, it may have been deleted or there is a configuration error.\",\n  \"noPromptAgain\": \"Do not prompt again\",\n  \"onServerDetailPage\": \"On server detail page\",\n  \"onlyOneLine\": \"Only display as one line (scrollable)\",\n  \"onlyWhenCoreBiggerThan8\": \"Works only when the number of cores is greater than 8\",\n  \"openLastPath\": \"Open the last path\",\n  \"openLastPathTip\": \"Different servers will have different logs, and the log is the path to the exit\",\n  \"parseContainerStatsTip\": \"Parsing the occupancy status of Docker is relatively slow.\",\n  \"percentOfSize\": \"{percent}% of {size}\",\n  \"permission\": \"Permissions\",\n  \"pingInputIP\": \"Please input a target IP / domain.\",\n  \"pingNoServer\": \"No server to ping.\\nPlease add a server in server tab.\",\n  \"plugInType\": \"Insertion Type\",\n  \"preferDiskAmount\": \"Prioritize displaying disk capacity\",\n  \"privateKey\": \"Private Key\",\n  \"privateKeyNotFoundFmt\": \"Private key [{keyId}] not found.\",\n  \"pushToken\": \"Push token\",\n  \"pveIgnoreCertTip\": \"Not recommended to enable, beware of security risks! If you are using the default certificate from PVE, you need to enable this option.\",\n  \"pveLoginFailed\": \"Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.\",\n  \"pveVersionLow\": \"This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.\",\n  \"read\": \"Read\",\n  \"recentConnections\": \"Recent Connections\",\n  \"rememberPwdInMem\": \"Remember password in memory\",\n  \"rememberPwdInMemTip\": \"Used for containers, suspending, etc.\",\n  \"rememberWindowSize\": \"Remember window size\",\n  \"remotePath\": \"Remote path\",\n  \"result\": \"Result\",\n  \"rotateAngel\": \"Rotation angle\",\n  \"sameIdServerExist\": \"A server with the same ID already exists\",\n  \"save\": \"Save\",\n  \"second\": \"s\",\n  \"serverDetailOrder\": \"Detail page widget order\",\n  \"serverFuncBtns\": \"Server function buttons\",\n  \"serverOrder\": \"Server order\",\n  \"serverTabRequired\": \"Server tab cannot be removed\",\n  \"sftpDlPrepare\": \"Preparing to connect...\",\n  \"sftpEditorTip\": \"If empty, use the built-in file editor of the app. If a value is present, use the remote server’s editor, e.g., `vim` (recommended to automatically detect according to `EDITOR`).\",\n  \"sftpRmrDirSummary\": \"Use `rm -r` to delete a folder in SFTP.\",\n  \"sftpSSHConnected\": \"SFTP Connected\",\n  \"sftpShowFoldersFirst\": \"Display folders first\",\n  \"showDistLogo\": \"Show distribution logo\",\n  \"size\": \"Size\",\n  \"softWrap\": \"Soft wrap\",\n  \"specifyDev\": \"Specify device\",\n  \"specifyDevTip\": \"For example, network traffic statistics are by default for all devices. You can specify a particular device here.\",\n  \"speed\": \"Speed\",\n  \"spentTime\": \"Spent time: {time}\",\n  \"sshConfigAllExist\": \"All servers already exist ({duplicateCount} duplicates found)\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} duplicates will be skipped\",\n  \"sshConfigFound\": \"We found SSH configuration on your system.\",\n  \"sshConfigFoundServers\": \"Found {totalCount} servers\",\n  \"sshConfigImport\": \"SSH Config Import\",\n  \"sshConfigImportHelp\": \"Only basic information can be imported, for example: IP/Port.\",\n  \"sshConfigImportPermission\": \"Would you like to give permission to read ~/.ssh/config and automatically import server settings?\",\n  \"sshConfigImportTip\": \"Prompt to read ~/.ssh/config on first server creation\",\n  \"sshConfigImported\": \"Imported {count} servers from SSH config\",\n  \"sshHostKeyChangedDesc\": \"The SSH host key changed for {serverName}. Only continue if you trust this server.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Fingerprint (MD5 base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Fingerprint (MD5 hex): {fingerprint}\",\n  \"sshHostKeyType\": \"SSH host key type\",\n  \"@sshHostKeyType\": {\n    \"description\": \"Label for the SSH host key type displayed in the host key verification dialog.\"\n  },\n  \"sshHostKeyNewDesc\": \"A new SSH host key was received from {serverName}. Review the fingerprint before trusting.\",\n  \"sshHostKeyStoredFingerprint\": \"Stored fingerprint: {fingerprint}\",\n  \"sshConfigManualSelect\": \"Would you like to select the SSH config file manually?\",\n  \"sshConfigNoServers\": \"No servers found in SSH config\",\n  \"sshConfigPermissionDenied\": \"Cannot access SSH config file due to macOS permissions.\",\n  \"sshConfigServersToImport\": \"{importCount} servers will be imported\",\n  \"sshTermHelp\": \"When the terminal is scrollable, dragging horizontally can select text. Clicking the keyboard button turns the keyboard on/off. The file icon opens the current path SFTP. The clipboard button copies the content when text is selected, and pastes content from the clipboard into the terminal when no text is selected and there is content on the clipboard. The code icon pastes code snippets into the terminal and executes them.\",\n  \"sshTip\": \"This function is now in the experimental stage.\\n\\nPlease report bugs on {url} or join our development.\",\n  \"sshVirtualKeyAutoOff\": \"Auto switching of virtual keys\",\n  \"stat\": \"Statistics\",\n  \"supportFmtArgs\": \"The following formatting parameters are supported:\",\n  \"suspendTip\": \"The suspend function requires root permission and systemd support.\",\n  \"switchTo\": \"Switch to {val}\",\n  \"syncTip\": \"A restart may be required for some changes to take effect.\",\n  \"system\": \"System\",\n  \"tag\": \"Tags\",\n  \"tapToStartDiscovery\": \"Tap the search button to discover SSH servers on your network\",\n  \"termFontSizeTip\": \"This setting will affect the terminal size (width and height). You can zoom in on the terminal page to adjust the font size of the current session.\",\n  \"textScaler\": \"Text scaler\",\n  \"textScalerTip\": \"1.0 => 100% (original size), only works on server page part of the font, not recommended to change.\",\n  \"time\": \"Time\",\n  \"times\": \"Times\",\n  \"trySudo\": \"Try using sudo\",\n  \"unknown\": \"Unknown\",\n  \"unkownConvertMode\": \"Unknown conversion mode\",\n  \"update\": \"Update\",\n  \"updateIntervalEqual0\": \"You set to 0, will not update automatically.\\nCan't calculate CPU status.\",\n  \"updateServerStatusInterval\": \"Server status update interval\",\n  \"upsideDown\": \"Upside Down\",\n  \"useCdn\": \"Using CDN\",\n  \"useCdnTip\": \"Non-Chinese users are recommended to use CDN. Would you like to use it?\",\n  \"useNoPwd\": \"No password will be used\",\n  \"usePodmanByDefault\": \"Use Podman by default\",\n  \"used\": \"Used\",\n  \"view\": \"View\",\n  \"viewDetails\": \"View Details\",\n  \"viewErr\": \"See error\",\n  \"virtKeyHelpClipboard\": \"Copy to the clipboard if the selected terminal is not empty, otherwise paste the content of the clipboard to the terminal.\",\n  \"virtKeyHelpIME\": \"Turn on/off the keyboard\",\n  \"virtKeyHelpSFTP\": \"Open current directory in SFTP.\",\n  \"waitConnection\": \"Please wait for the connection to be established.\",\n  \"wakeLock\": \"Keep awake\",\n  \"watchNotPaired\": \"No paired Apple Watch\",\n  \"webdavSettingEmpty\": \"WebDav setting is empty\",\n  \"whenOpenApp\": \"When opening the app\",\n  \"wolTip\": \"After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.\",\n  \"write\": \"Write\",\n  \"writeScriptFailTip\": \"Writing to the script failed, possibly due to lack of permissions or the directory does not exist.\",\n  \"writeScriptTip\": \"After connecting to the server, a script will be written to `~/.config/server_box` \\n | `/tmp/server_box` to monitor the system status. You can review the script content.\",\n  \"menuGitHubRepository\": \"GitHub Repository\",\n  \"podmanDockerEmulationDetected\": \"Podman Docker emulation detected. Please switch to Podman in settings.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_es.arb",
    "content": "{\n  \"@@locale\": \"es\",\n  \"aboutThanks\": \"Gracias a los siguientes participantes.\",\n  \"acceptBeta\": \"Aceptar actualizaciones de la versión de prueba\",\n  \"addSystemPrivateKeyTip\": \"Actualmente no hay ninguna llave privada, ¿quieres agregar la que viene por defecto en el sistema (~/.ssh/id_rsa)?\",\n  \"added2List\": \"Añadido a la lista de tareas\",\n  \"addr\": \"Dirección\",\n  \"alreadyLastDir\": \"Ya estás en el directorio superior\",\n  \"askAi\": \"Preguntar a la IA\",\n  \"askAiApiKey\": \"Clave API\",\n  \"askAiAwaitingResponse\": \"Esperando la respuesta de la IA...\",\n  \"askAiBaseUrl\": \"URL base\",\n  \"askAiCommandInserted\": \"Comando insertado en el terminal\",\n  \"askAiConfigMissing\": \"Configura {fields} en Ajustes.\",\n  \"askAiConfirmExecute\": \"Confirmar antes de ejecutar\",\n  \"askAiConversation\": \"Conversación con la IA\",\n  \"askAiDisclaimer\": \"La IA puede equivocarse. Úsala con precaución.\",\n  \"askAiFollowUpHint\": \"Haz una pregunta adicional...\",\n  \"askAiInsertTerminal\": \"Insertar en el terminal\",\n  \"askAiNoResponse\": \"Sin respuesta\",\n  \"askAiRecommendedCommand\": \"Comando sugerido por la IA\",\n  \"askAiSelectedContent\": \"Contenido seleccionado\",\n  \"askAiUsageHint\": \"Usado en el terminal SSH\",\n  \"atLeastOneTab\": \"Al menos una pestaña debe estar seleccionada\",\n  \"authFailTip\": \"La autenticación ha fallado, por favor verifica si la contraseña/llave/host/usuario, etc., son incorrectos.\",\n  \"autoBackupConflict\": \"Solo se puede activar una copia de seguridad automática a la vez\",\n  \"autoConnect\": \"Conexión automática\",\n  \"autoRun\": \"Ejecución automática\",\n  \"autoUpdateHomeWidget\": \"Actualizar automáticamente el widget del escritorio\",\n  \"availableTabs\": \"Pestañas disponibles\",\n  \"backupEncrypted\": \"El respaldo está encriptado\",\n  \"backupNotEncrypted\": \"El respaldo no está encriptado\",\n  \"backupPassword\": \"Contraseña de respaldo\",\n  \"backupPasswordRemoved\": \"Contraseña de respaldo eliminada\",\n  \"backupPasswordSet\": \"Contraseña de respaldo establecida\",\n  \"backupPasswordTip\": \"Establece una contraseña para encriptar archivos de respaldo. Déjalo vacío para desactivar la encriptación.\",\n  \"backupPasswordWrong\": \"Contraseña de respaldo incorrecta\",\n  \"backupTip\": \"Los datos exportados pueden ser encriptados con contraseña. \\nPor favor guárdalos en un lugar seguro.\",\n  \"backupVersionNotMatch\": \"La versión de la copia de seguridad no coincide, no se puede restaurar\",\n  \"bgRun\": \"Ejecución en segundo plano\",\n  \"bgRunTip\": \"Este interruptor solo indica que la aplicación intentará correr en segundo plano, si puede hacerlo o no depende de si tiene el permiso correspondiente. En Android puro, por favor desactiva la “optimización de batería” para esta app, en MIUI por favor cambia la estrategia de ahorro de energía a “Sin restricciones”.\",\n  \"clearAllStatsContent\": \"¿Estás seguro de que quieres limpiar todas las estadísticas de conexión del servidor? Esta acción no se puede deshacer.\",\n  \"clearAllStatsTitle\": \"Limpiar todas las estadísticas\",\n  \"clearServerStatsContent\": \"¿Estás seguro de que quieres limpiar las estadísticas de conexión del servidor \\\"{serverName}\\\"? Esta acción no se puede deshacer.\",\n  \"clearServerStatsTitle\": \"Limpiar estadísticas de {serverName}\",\n  \"clearThisServerStats\": \"Limpiar estadísticas de este servidor\",\n  \"compactDatabase\": \"Compactar base de datos\",\n  \"compactDatabaseContent\": \"Tamaño de la base de datos: {size}\\n\\nEsto reorganizará la base de datos para reducir el tamaño del archivo. No se eliminará ningún dato.\",\n  \"closeAfterSave\": \"Guardar y cerrar\",\n  \"collapseUITip\": \"¿Colapsar por defecto las listas largas en la UI?\",\n  \"connectionDetails\": \"Detalles de conexión\",\n  \"connectionStats\": \"Estadísticas de conexión\",\n  \"connectionStatsDesc\": \"Ver la tasa de éxito de conexión del servidor e historial\",\n  \"containerTrySudoTip\": \"Por ejemplo: si configuras el usuario dentro de la app como aaa, pero Docker está instalado bajo el usuario root, entonces necesitarás habilitar esta opción\",\n  \"containerSudoPasswordRequired\": \"Se requiere contraseña de sudo para acceder a Docker. Por favor ingrese su contraseña.\",\n  \"containerSudoPasswordIncorrect\": \"La contraseña de sudo es incorrecta o no está permitida. Por favor intente de nuevo.\",\n  \"convert\": \"Convertir\",\n  \"copyPath\": \"Copiar ruta\",\n  \"cpuViewAsProgressTip\": \"Muestre la tasa de uso de cada CPU en estilo de barra de progreso (estilo antiguo)\",\n  \"cursorType\": \"Tipo de cursor\",\n  \"customCmd\": \"Comandos personalizados\",\n  \"customCmdHint\": \"\\\"Nombre del comando\\\": \\\"Comando\\\"\",\n  \"deleteServers\": \"Eliminar servidores en lote\",\n  \"desktopTerminalTip\": \"Comando utilizado para abrir el emulador de terminal al iniciar sesiones SSH.\",\n  \"dirEmpty\": \"Asegúrate de que el directorio esté vacío\",\n  \"discoverSshServers\": \"Descubrir servidores SSH\",\n  \"discoveryFailed\": \"Falló el descubrimiento\",\n  \"discoverySettings\": \"Configuración de descubrimiento\",\n  \"discoverySummary\": \"Resumen del descubrimiento\",\n  \"diskHealth\": \"Salud del disco\",\n  \"diskIgnorePath\": \"Rutas de disco ignoradas\",\n  \"displayCpuIndex\": \"Muestre el índice de CPU\",\n  \"dl2Local\": \"¿Descargar {fileName} a local?\",\n  \"dockerEmptyRunningItems\": \"No hay contenedores en ejecución.\\nEsto podría deberse a que:\\n- El usuario con el que se instaló Docker es diferente al configurado en la app\\n- La variable de entorno DOCKER_HOST no se ha leído correctamente. Puedes obtenerla ejecutando `echo $DOCKER_HOST` en el terminal.\",\n  \"dockerImagesFmt\": \"Total de {count} imágenes\",\n  \"dockerNotInstalled\": \"Docker no está instalado\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} en ejecución, {stoppedCount} detenidos\",\n  \"dockerStatusRunningFmt\": \"{count} contenedores en ejecución\",\n  \"doubleColumnMode\": \"Modo de doble columna\",\n  \"doubleColumnTip\": \"Esta opción solo habilita la función, si se puede activar o no depende del ancho del dispositivo\",\n  \"editVirtKeys\": \"Editar teclas virtuales\",\n  \"editorHighlightTip\": \"El rendimiento del resaltado de código es bastante pobre actualmente, puedes elegir desactivarlo para mejorar.\",\n  \"enableMdns\": \"Habilitar mDNS\",\n  \"enableMdnsDesc\": \"Usar mDNS/Bonjour para descubrir servicios SSH\",\n  \"envVars\": \"Variable de entorno\",\n  \"experimentalFeature\": \"Función experimental\",\n  \"extraArgs\": \"Argumentos extra\",\n  \"fallbackSshDest\": \"Destino SSH alternativo\",\n  \"fdroidReleaseTip\": \"Si descargaste esta aplicación desde F-Droid, se recomienda desactivar esta opción.\",\n  \"fgService\": \"Servicio en primer plano\",\n  \"fgServiceTip\": \"Después de activarlo, algunos modelos de dispositivos pueden bloquearse. Desactivarlo puede hacer que algunos modelos no puedan mantener las conexiones SSH en segundo plano. Por favor, permita los permisos de notificación de ServerBox, la ejecución en segundo plano y el auto-despertar en la configuración del sistema.\",\n  \"fileTooLarge\": \"El archivo '{file}' es demasiado grande '{size}', supera el {sizeMax}\",\n  \"finishedAt\": \"Terminado en\",\n  \"followSystem\": \"Seguir al sistema\",\n  \"fontSize\": \"Tamaño de fuente\",\n  \"fullScreen\": \"Modo pantalla completa\",\n  \"fullScreenJitter\": \"Temblores en modo pantalla completa\",\n  \"fullScreenJitterHelp\": \"Prevención de quemaduras de pantalla\",\n  \"fullScreenTip\": \"¿Debe habilitarse el modo de pantalla completa cuando el dispositivo se rote al modo horizontal? Esta opción solo se aplica a la pestaña del servidor.\",\n  \"goBackQ\": \"¿Regresar?\",\n  \"goto\": \"Ir a\",\n  \"hideTitleBar\": \"Ocultar barra de título\",\n  \"highlight\": \"Resaltar código\",\n  \"homeTabs\": \"Pestañas de inicio\",\n  \"homeTabsCustomizeDesc\": \"Personaliza qué pestañas aparecen en la página de inicio y su orden\",\n  \"homeWidgetUrlConfig\": \"Configuración de URL del widget de inicio\",\n  \"httpFailedWithCode\": \"Fallo en la solicitud, código de estado: {code}\",\n  \"ignoreCert\": \"Ignorar certificado\",\n  \"image\": \"Imagen\",\n  \"imagesList\": \"Lista de imágenes\",\n  \"installDockerWithUrl\": \"Por favor instala Docker primero desde https://docs.docker.com/engine/install\",\n  \"invalid\": \"Inválido\",\n  \"jumpServer\": \"Servidor de salto\",\n  \"keepForeground\": \"¡Por favor, mantén la app en primer plano!\",\n  \"keepStatusWhenErr\": \"Mantener el estado anterior del servidor\",\n  \"keepStatusWhenErrTip\": \"Solo aplica cuando hay errores al ejecutar scripts\",\n  \"keyAuth\": \"Autenticación con llave\",\n  \"lastFailure\": \"Último fallo\",\n  \"lastSuccess\": \"Último éxito\",\n  \"letterCache\": \"Caché de letras\",\n  \"letterCacheTip\": \"Recomendado desactivar, pero después de desactivarlo, no se podrán ingresar caracteres CJK.\",\n  \"madeWithLove\": \"Hecho con ❤️ por {myGithub}\",\n  \"max\": \"Máximo\",\n  \"maxConcurrency\": \"Concurrencia máxima\",\n  \"maxRetryCount\": \"Número máximo de reintentos de conexión al servidor\",\n  \"maxRetryCountEqual0\": \"Reintentará infinitamente\",\n  \"min\": \"Mínimo\",\n  \"more\": \"Más\",\n  \"moveOutServerFuncBtnsHelp\": \"Activado: se mostrará debajo de cada tarjeta en la página de servidores. Desactivado: se mostrará en la parte superior de los detalles del servidor.\",\n  \"needHomeDir\": \"Si eres usuario de Synology, [consulta aquí](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Los usuarios de otros sistemas deben buscar cómo crear un directorio home.\",\n  \"needRestart\": \"Necesita reiniciar la app\",\n  \"netViewType\": \"Tipo de vista de red\",\n  \"newContainer\": \"Crear contenedor nuevo\",\n  \"noConnectionStatsData\": \"No hay datos de estadísticas de conexión\",\n  \"noLineChart\": \"No utilice gráficos de líneas\",\n  \"noLineChartForCpu\": \"No utilice gráficos lineales para la CPU\",\n  \"noPrivateKeyTip\": \"La clave privada no existe, puede haber sido eliminada o hay un error de configuración.\",\n  \"noPromptAgain\": \"No volver a preguntar\",\n  \"onServerDetailPage\": \"En la página de detalles del servidor\",\n  \"onlyOneLine\": \"Mostrar solo en una línea (desplazable)\",\n  \"onlyWhenCoreBiggerThan8\": \"Efectivo solo cuando el número de núcleos > 8\",\n  \"openLastPath\": \"Abrir el último camino\",\n  \"openLastPathTip\": \"Los diferentes servidores tendrán diferentes registros, y lo que se registra es la ruta de salida\",\n  \"parseContainerStatsTip\": \"El análisis del estado de uso de Docker es bastante lento\",\n  \"percentOfSize\": \"El {percent}% de {size}\",\n  \"permission\": \"Permisos\",\n  \"pingInputIP\": \"Por favor, introduce la IP de destino o el dominio\",\n  \"pingNoServer\": \"No hay servidores disponibles para hacer Ping\\nPor favor, añade un servidor en la pestaña de servidores y vuelve a intentarlo\",\n  \"plugInType\": \"Tipo de inserción\",\n  \"preferDiskAmount\": \"Priorizar la visualización de la capacidad del disco\",\n  \"privateKey\": \"Llave privada\",\n  \"privateKeyNotFoundFmt\": \"No se encontró la clave privada [{keyId}].\",\n  \"pushToken\": \"Token de notificaciones\",\n  \"pveIgnoreCertTip\": \"No se recomienda activarlo, ¡tenga cuidado con los riesgos de seguridad! Si está utilizando el certificado predeterminado de PVE, debe habilitar esta opción.\",\n  \"pveLoginFailed\": \"Fallo al iniciar sesión. No se puede autenticar con el nombre de usuario/contraseña de la configuración del servidor para el inicio de sesión de Linux PAM.\",\n  \"pveVersionLow\": \"Esta función está actualmente en fase de prueba y solo se ha probado en PVE 8+. Úsela con precaución.\",\n  \"read\": \"Leer\",\n  \"recentConnections\": \"Conexiones recientes\",\n  \"rememberPwdInMem\": \"Recordar contraseña en la memoria\",\n  \"rememberPwdInMemTip\": \"Utilizado para contenedores, suspensión, etc.\",\n  \"rememberWindowSize\": \"Recordar el tamaño de la ventana\",\n  \"remotePath\": \"Ruta remota\",\n  \"result\": \"Resultado\",\n  \"rotateAngel\": \"Ángulo de rotación\",\n  \"sameIdServerExist\": \"Ya existe un servidor con el mismo ID\",\n  \"save\": \"Guardar\",\n  \"second\": \"Segundo\",\n  \"serverDetailOrder\": \"Orden de los componentes en la página de detalles del servidor\",\n  \"serverFuncBtns\": \"Botones de función del servidor\",\n  \"serverOrder\": \"Orden del servidor\",\n  \"serverTabRequired\": \"La pestaña del servidor no se puede eliminar\",\n  \"sftpDlPrepare\": \"Preparando para conectar al servidor...\",\n  \"sftpEditorTip\": \"Si está vacío, use el editor de archivos incorporado de la aplicación. Si hay un valor, use el editor del servidor remoto, por ejemplo, `vim` (se recomienda detectar automáticamente según `EDITOR`).\",\n  \"sftpRmrDirSummary\": \"Usar `rm -r` en SFTP para eliminar directorios\",\n  \"sftpSSHConnected\": \"SFTP conectado...\",\n  \"sftpShowFoldersFirst\": \"Mostrar carpetas primero\",\n  \"showDistLogo\": \"Mostrar logo de distribución\",\n  \"size\": \"Tamaño\",\n  \"softWrap\": \"Salto de línea suave\",\n  \"specifyDev\": \"Especificar dispositivo\",\n  \"specifyDevTip\": \"Por ejemplo, las estadísticas de tráfico de red son por defecto para todos los dispositivos. Aquí puede especificar un dispositivo en particular.\",\n  \"speed\": \"Velocidad\",\n  \"spentTime\": \"Tiempo gastado: {time}\",\n  \"sshConfigAllExist\": \"Todos los servidores ya existen (se encontraron {duplicateCount} duplicados)\",\n  \"sshConfigDuplicatesSkipped\": \"Se omitirán {duplicateCount} duplicados\",\n  \"sshConfigFound\": \"Encontramos configuración SSH en tu sistema\",\n  \"sshConfigFoundServers\": \"Se encontraron {totalCount} servidores\",\n  \"sshConfigImport\": \"Importar Configuración SSH\",\n  \"sshConfigImportHelp\": \"Solo se pueden importar datos básicos, por ejemplo: IP/Puerto.\",\n  \"sshConfigImportPermission\": \"¿Te gustaría dar permiso para leer ~/.ssh/config e importar automáticamente la configuración de servidores?\",\n  \"sshConfigImportTip\": \"Sugerencia para leer ~/.ssh/config al crear el primer servidor\",\n  \"sshConfigImported\": \"Se importaron {count} servidores desde la configuración SSH\",\n  \"sshHostKeyChangedDesc\": \"La clave de host SSH de {serverName} ha cambiado. Continúa solo si confías en este servidor.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Huella (MD5 Base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Huella (MD5 hex): {fingerprint}\",\n  \"sshHostKeyType\": \"Tipo de clave de host SSH\",\n  \"sshHostKeyNewDesc\": \"Se recibió una nueva clave de host SSH de {serverName}. Revisa la huella antes de confiar.\",\n  \"sshHostKeyStoredFingerprint\": \"Huella almacenada: {fingerprint}\",\n  \"sshConfigManualSelect\": \"¿Te gustaría seleccionar manualmente el archivo de configuración SSH?\",\n  \"sshConfigNoServers\": \"No se encontraron servidores en la configuración SSH\",\n  \"sshConfigPermissionDenied\": \"No se puede acceder al archivo de configuración SSH debido a los permisos de macOS.\",\n  \"sshConfigServersToImport\": \"Se importarán {importCount} servidores\",\n  \"sshTermHelp\": \"Cuando el terminal es desplazable, arrastrar horizontalmente puede seleccionar texto. Hacer clic en el botón del teclado enciende/apaga el teclado. El icono de archivo abre el SFTP de la ruta actual. El botón del portapapeles copia el contenido cuando se selecciona texto y pega el contenido del portapapeles en el terminal cuando no se selecciona texto y hay contenido en el portapapeles. El icono de código pega fragmentos de código en el terminal y los ejecuta.\",\n  \"sshTip\": \"Esta función está en fase de pruebas.\\n\\nPor favor, informa los problemas en {url}, o únete a nuestro desarrollo.\",\n  \"sshVirtualKeyAutoOff\": \"Desactivación automática de teclas virtuales\",\n  \"stat\": \"Estadísticas\",\n  \"supportFmtArgs\": \"Soporta los siguientes argumentos de formato:\",\n  \"suspendTip\": \"La función de suspender necesita permisos de root y soporte de systemd.\",\n  \"switchTo\": \"Cambiar a {val}\",\n  \"syncTip\": \"Puede que necesites reiniciar para que algunos cambios tengan efecto.\",\n  \"system\": \"Sistema\",\n  \"tag\": \"Etiqueta\",\n  \"tapToStartDiscovery\": \"Toca el botón de búsqueda para descubrir servidores SSH en tu red\",\n  \"termFontSizeTip\": \"Este ajuste afectará el tamaño del terminal (ancho y alto). Puedes hacer zoom en la página del terminal para ajustar el tamaño de fuente de la sesión actual.\",\n  \"textScaler\": \"Escalar texto\",\n  \"textScalerTip\": \"1.0 => 100% (tamaño original), solo afecta a ciertas fuentes en la página del servidor, no se recomienda modificar.\",\n  \"time\": \"Tiempo\",\n  \"times\": \"Veces\",\n  \"trySudo\": \"Intentar con sudo\",\n  \"unknown\": \"Desconocido\",\n  \"unkownConvertMode\": \"Modo de conversión desconocido\",\n  \"update\": \"Actualizar\",\n  \"updateIntervalEqual0\": \"Si configuras esto a 0, el estado del servidor no se refrescará automáticamente.\\nY no se podrá calcular el uso de CPU.\",\n  \"updateServerStatusInterval\": \"Intervalo de actualización del estado del servidor\",\n  \"upsideDown\": \"Invertir arriba por abajo\",\n  \"useCdn\": \"Usando CDN\",\n  \"useCdnTip\": \"Se recomienda a los usuarios no chinos utilizar CDN. ¿Le gustaría utilizarlo?\",\n  \"useNoPwd\": \"Se usará sin contraseña\",\n  \"usePodmanByDefault\": \"Usar Podman por defecto\",\n  \"used\": \"Usado\",\n  \"view\": \"Vista\",\n  \"viewDetails\": \"Ver detalles\",\n  \"viewErr\": \"Ver error\",\n  \"virtKeyHelpClipboard\": \"Si el terminal tiene caracteres seleccionados, entonces copiará los caracteres seleccionados al portapapeles, de lo contrario, pegará el contenido del portapapeles al terminal.\",\n  \"virtKeyHelpIME\": \"Encender/apagar el teclado\",\n  \"virtKeyHelpSFTP\": \"Abrir la ruta actual en SFTP.\",\n  \"waitConnection\": \"Por favor, espera a que la conexión se establezca\",\n  \"wakeLock\": \"Mantener despierto\",\n  \"watchNotPaired\": \"No hay un Apple Watch emparejado\",\n  \"webdavSettingEmpty\": \"La configuración de Webdav está vacía\",\n  \"whenOpenApp\": \"Al abrir la App\",\n  \"wolTip\": \"Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.\",\n  \"write\": \"Escribir\",\n  \"writeScriptFailTip\": \"La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.\",\n  \"writeScriptTip\": \"Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \\n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.\",\n  \"podmanDockerEmulationDetected\": \"Detectada emulación de Podman Docker. Por favor, cambie a Podman en la configuración.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_fr.arb",
    "content": "{\n  \"@@locale\": \"fr\",\n  \"aboutThanks\": \"Merci aux personnes suivantes qui ont participé.\",\n  \"acceptBeta\": \"Accepter les mises à jour de la version de test\",\n  \"addSystemPrivateKeyTip\": \"Actuellement, vous n'avez aucune clé privée. Souhaitez-vous ajouter celle qui vient avec le système (~/.ssh/id_rsa) ?\",\n  \"added2List\": \"Ajouté à la liste des tâches\",\n  \"addr\": \"Adresse\",\n  \"alreadyLastDir\": \"Déjà dans le dernier répertoire.\",\n  \"askAi\": \"Demander à l'IA\",\n  \"askAiApiKey\": \"Clé API\",\n  \"askAiAwaitingResponse\": \"En attente de la réponse de l'IA...\",\n  \"askAiBaseUrl\": \"URL de base\",\n  \"askAiCommandInserted\": \"Commande insérée dans le terminal\",\n  \"askAiConfigMissing\": \"Veuillez configurer {fields} dans les paramètres.\",\n  \"askAiConfirmExecute\": \"Confirmer avant d'exécuter\",\n  \"askAiConversation\": \"Conversation avec l'IA\",\n  \"askAiDisclaimer\": \"L'IA peut se tromper. Utilisez-la avec prudence.\",\n  \"askAiFollowUpHint\": \"Poser une question supplémentaire...\",\n  \"askAiInsertTerminal\": \"Insérer dans le terminal\",\n  \"askAiNoResponse\": \"Aucune réponse\",\n  \"askAiRecommendedCommand\": \"Commande suggérée par l'IA\",\n  \"askAiSelectedContent\": \"Contenu sélectionné\",\n  \"askAiUsageHint\": \"Utilisé dans le terminal SSH\",\n  \"atLeastOneTab\": \"Au moins un onglet doit être sélectionné\",\n  \"authFailTip\": \"Échec de l'authentification. Veuillez vérifier si le mot de passe/clé/hôte/utilisateur, etc., est incorrect.\",\n  \"autoBackupConflict\": \"Un seul sauvegarde automatique peut être activé en même temps.\",\n  \"autoConnect\": \"Connexion automatique\",\n  \"autoRun\": \"Exécution automatique\",\n  \"autoUpdateHomeWidget\": \"Mise à jour automatique du widget d'accueil\",\n  \"availableTabs\": \"Onglets disponibles\",\n  \"backupEncrypted\": \"La sauvegarde est chiffrée\",\n  \"backupNotEncrypted\": \"La sauvegarde n'est pas chiffrée\",\n  \"backupPassword\": \"Mot de passe de sauvegarde\",\n  \"backupPasswordRemoved\": \"Mot de passe de sauvegarde supprimé\",\n  \"backupPasswordSet\": \"Mot de passe de sauvegarde défini\",\n  \"backupPasswordTip\": \"Définissez un mot de passe pour chiffrer les fichiers de sauvegarde. Laissez vide pour désactiver le chiffrement.\",\n  \"backupPasswordWrong\": \"Mot de passe de sauvegarde incorrect\",\n  \"backupTip\": \"Les données exportées peuvent être chiffrées avec un mot de passe. \\nVeuillez les garder en sécurité.\",\n  \"backupVersionNotMatch\": \"La version de sauvegarde ne correspond pas.\",\n  \"bgRun\": \"Exécution en arrière-plan\",\n  \"bgRunTip\": \"Cette option signifie seulement que le programme essaiera de s'exécuter en arrière-plan, que cela soit possible dépend de l'autorisation activée ou non. Pour Android natif, veuillez désactiver l'« Optimisation de la batterie » dans cette application, et pour MIUI, veuillez changer la politique d'économie d'énergie en « Illimité ».\",\n  \"clearAllStatsContent\": \"Êtes-vous sûr de vouloir effacer toutes les statistiques de connexion des serveurs ? Cette action ne peut pas être annulée.\",\n  \"clearAllStatsTitle\": \"Effacer toutes les statistiques\",\n  \"clearServerStatsContent\": \"Êtes-vous sûr de vouloir effacer les statistiques de connexion du serveur \\\"{serverName}\\\" ? Cette action ne peut pas être annulée.\",\n  \"clearServerStatsTitle\": \"Effacer les statistiques de {serverName}\",\n  \"clearThisServerStats\": \"Effacer les statistiques de ce serveur\",\n  \"compactDatabase\": \"Compacter la base de données\",\n  \"compactDatabaseContent\": \"Taille de la base de données : {size}\\n\\nCela réorganisera la base de données pour réduire la taille du fichier. Aucune donnée ne sera supprimée.\",\n  \"closeAfterSave\": \"Enregistrer et fermer\",\n  \"collapseUITip\": \"Indique si les longues listes présentées dans l'interface utilisateur doivent être réduites par défaut.\",\n  \"connectionDetails\": \"Détails de connexion\",\n  \"connectionStats\": \"Statistiques de connexion\",\n  \"connectionStatsDesc\": \"Voir le taux de réussite de connexion du serveur et l'historique\",\n  \"containerTrySudoTip\": \"Par exemple : Dans l'application, l'utilisateur est défini comme aaa, mais Docker est installé sous l'utilisateur root. Dans ce cas, vous devez activer cette option.\",\n  \"containerSudoPasswordRequired\": \"Un mot de passe sudo est requis pour accéder à Docker. Veuillez entrer votre mot de passe.\",\n  \"containerSudoPasswordIncorrect\": \"Le mot de passe sudo est incorrect ou non autorisé. Veuillez réessayer.\",\n  \"convert\": \"Convertir\",\n  \"copyPath\": \"Copier le chemin\",\n  \"cpuViewAsProgressTip\": \"Afficher le taux d'utilisation de chaque CPU sous forme de barre de progression (ancien style)\",\n  \"cursorType\": \"Type de curseur\",\n  \"customCmd\": \"Commandes personnalisées\",\n  \"customCmdHint\": \"\\\"Nom de la commande\\\": \\\"Commande\\\"\",\n  \"deleteServers\": \"Supprimer des serveurs en lot\",\n  \"desktopTerminalTip\": \"Commande utilisée pour ouvrir l’émulateur de terminal lors du lancement de sessions SSH.\",\n  \"dirEmpty\": \"Assurez-vous que le répertoire est vide.\",\n  \"discoverSshServers\": \"Découvrir les serveurs SSH\",\n  \"discoveryFailed\": \"Échec de la découverte\",\n  \"discoverySettings\": \"Paramètres de découverte\",\n  \"discoverySummary\": \"Résumé de la découverte\",\n  \"diskHealth\": \"Santé du disque\",\n  \"diskIgnorePath\": \"Chemin à ignorer pour le disque\",\n  \"displayCpuIndex\": \"Afficher l'index CPU\",\n  \"dl2Local\": \"Télécharger {fileName} localement ?\",\n  \"dockerEmptyRunningItems\": \"Aucun conteneur en cours d'exécution.\\nCela peut être dû à :\\n- L'utilisateur d'installation de Docker n'est pas le même que celui configuré dans l'application.\\n- La variable d'environnement DOCKER_HOST n'a pas été lue correctement. Vous pouvez l'obtenir en exécutant `echo $DOCKER_HOST` dans le terminal.\",\n  \"dockerImagesFmt\": \"{count} images\",\n  \"dockerNotInstalled\": \"Docker non installé\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} en cours d'exécution, {stoppedCount} conteneur arrêté.\",\n  \"dockerStatusRunningFmt\": \"{count} conteneur en cours d'exécution.\",\n  \"doubleColumnMode\": \"Mode double colonne\",\n  \"doubleColumnTip\": \"Cette option n'active que la fonctionnalité, qu'elle puisse être activée dépend de la largeur de l'appareil.\",\n  \"editVirtKeys\": \"Modifier les touches virtuelles\",\n  \"editorHighlightTip\": \"La performance actuelle de mise en surbrillance du code est pire et peut être désactivée en option pour s'améliorer.\",\n  \"enableMdns\": \"Activer mDNS\",\n  \"enableMdnsDesc\": \"Utiliser mDNS/Bonjour pour découvrir les services SSH\",\n  \"envVars\": \"Variable d’environnement\",\n  \"experimentalFeature\": \"Fonctionnalité expérimentale\",\n  \"extraArgs\": \"Arguments supplémentaires\",\n  \"fallbackSshDest\": \"Destino SSH alternativo\",\n  \"fdroidReleaseTip\": \"Si vous avez téléchargé cette application depuis F-Droid, il est recommandé de désactiver cette option.\",\n  \"fgService\": \"Service de premier plan\",\n  \"fgServiceTip\": \"Après l'activation, certains modèles d'appareils peuvent planter. La désactivation peut empêcher certains modèles de maintenir les connexions SSH en arrière-plan. Veuillez autoriser les permissions de notification ServerBox, l'exécution en arrière-plan et l'auto-réveil dans les paramètres système.\",\n  \"fileTooLarge\": \"Fichier '{file}' trop volumineux {size}, max {sizeMax}\",\n  \"finishedAt\": \"Terminé à\",\n  \"followSystem\": \"Suivre le système\",\n  \"fontSize\": \"Taille de la police\",\n  \"fullScreen\": \"Mode plein écran\",\n  \"fullScreenJitter\": \"Secousse en plein écran\",\n  \"fullScreenJitterHelp\": \"Pour éviter les brûlures d'écran\",\n  \"fullScreenTip\": \"Le mode plein écran doit-il être activé lorsque l'appareil est orienté en mode paysage ? Cette option s'applique uniquement à l'onglet serveur.\",\n  \"goBackQ\": \"Revenir en arrière ?\",\n  \"goto\": \"Aller à\",\n  \"hideTitleBar\": \"Masquer la barre de titre\",\n  \"highlight\": \"Mise en surbrillance du code\",\n  \"homeTabs\": \"Onglets d'accueil\",\n  \"homeTabsCustomizeDesc\": \"Personnalisez les onglets qui apparaissent sur la page d'accueil et leur ordre\",\n  \"homeWidgetUrlConfig\": \"Configurer l'URL du widget d'accueil\",\n  \"httpFailedWithCode\": \"Échec de la requête, code d'état : {code}\",\n  \"ignoreCert\": \"Ignorer le certificat\",\n  \"image\": \"Image\",\n  \"imagesList\": \"Liste des images\",\n  \"installDockerWithUrl\": \"Veuillez d'abord installer docker depuis https://docs.docker.com/engine/install.\",\n  \"invalid\": \"Invalide\",\n  \"jumpServer\": \"Aller au serveur\",\n  \"keepForeground\": \"Garder l'application en premier plan !\",\n  \"keepStatusWhenErr\": \"Conserver l'état du dernier serveur\",\n  \"keepStatusWhenErrTip\": \"Uniquement en cas d'erreur lors de l'exécution du script\",\n  \"keyAuth\": \"Authentification par clé\",\n  \"lastFailure\": \"Dernier échec\",\n  \"lastSuccess\": \"Dernier succès\",\n  \"letterCache\": \"Mise en cache des lettres\",\n  \"letterCacheTip\": \"Recommandé de désactiver, mais après désactivation, il sera impossible de saisir des caractères CJK.\",\n  \"madeWithLove\": \"Fabriqué avec ❤️ par {myGithub}\",\n  \"max\": \"max\",\n  \"maxConcurrency\": \"Concurrence maximale\",\n  \"maxRetryCount\": \"Nombre de reconnexions au serveur\",\n  \"maxRetryCountEqual0\": \"Il va réessayer encore et encore.\",\n  \"min\": \"min\",\n  \"more\": \"Plus\",\n  \"moveOutServerFuncBtnsHelp\": \"Activé : peut être affiché sous chaque carte sur la page de l'onglet Serveur. Désactivé : peut être affiché en haut de la page de détails du serveur.\",\n  \"needHomeDir\": \"Si vous êtes utilisateur Synology, [consultez ici](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Les utilisateurs d'autres systèmes doivent rechercher comment créer un répertoire personnel.\",\n  \"needRestart\": \"Nécessite un redémarrage de l'application\",\n  \"netViewType\": \"Type de vue réseau\",\n  \"newContainer\": \"Nouveau conteneur\",\n  \"noConnectionStatsData\": \"Aucune donnée de statistiques de connexion\",\n  \"noLineChart\": \"Ne pas utiliser de graphiques linéaires\",\n  \"noLineChartForCpu\": \"Ne pas utiliser de graphiques linéaires pour l'unité centrale\",\n  \"noPrivateKeyTip\": \"La clé privée n'existe pas, elle a peut-être été supprimée ou il y a une erreur de configuration.\",\n  \"noPromptAgain\": \"Ne pas demander à nouveau\",\n  \"onServerDetailPage\": \"Sur la page de détails du serveur\",\n  \"onlyOneLine\": \"Afficher uniquement en une seule ligne (défilement)\",\n  \"onlyWhenCoreBiggerThan8\": \"Fonctionne uniquement lorsque le nombre de cœurs est > 8\",\n  \"openLastPath\": \"Ouvrir le dernier chemin\",\n  \"openLastPathTip\": \"Les différents serveurs auront des journaux différents, et le journal est le chemin vers la sortie\",\n  \"parseContainerStatsTip\": \"L'analyse de l'occupation des conteneurs Docker est relativement lente.\",\n  \"percentOfSize\": \"{percent}% de {size}\",\n  \"permission\": \"Permissions\",\n  \"pingInputIP\": \"Veuillez saisir une adresse IP / un domaine cible.\",\n  \"pingNoServer\": \"Aucun serveur à pinger.\\nVeuillez ajouter un serveur dans l'onglet serveur.\",\n  \"plugInType\": \"Type d'insertion\",\n  \"preferDiskAmount\": \"Prioriser l’affichage de la capacité du disque\",\n  \"privateKey\": \"Clé privée\",\n  \"privateKeyNotFoundFmt\": \"Clé privée [{keyId}] introuvable.\",\n  \"pushToken\": \"Jeton d'identification\",\n  \"pveIgnoreCertTip\": \"Il n'est pas recommandé de l'activer, attention aux risques de sécurité ! Si vous utilisez le certificat par défaut de PVE, vous devez activer cette option.\",\n  \"pveLoginFailed\": \"Échec de la connexion. Impossible d'authentifier avec le nom d'utilisateur / mot de passe de la configuration du serveur pour la connexion Linux PAM.\",\n  \"pveVersionLow\": \"Cette fonctionnalité est actuellement en phase de test et n'a été testée que sur PVE 8+. Veuillez l'utiliser avec prudence.\",\n  \"read\": \"Lire\",\n  \"recentConnections\": \"Connexions récentes\",\n  \"rememberPwdInMem\": \"Mémoriser le mot de passe en mémoire\",\n  \"rememberPwdInMemTip\": \"Utilisé pour les conteneurs, la suspension, etc.\",\n  \"rememberWindowSize\": \"Se souvenir de la taille de la fenêtre\",\n  \"remotePath\": \"Chemin distant\",\n  \"result\": \"Résultat\",\n  \"rotateAngel\": \"Angle de rotation\",\n  \"sameIdServerExist\": \"Un serveur avec le même ID existe déjà\",\n  \"save\": \"Enregistrer\",\n  \"second\": \"s\",\n  \"serverDetailOrder\": \"Ordre des widgets de la page de détails du serveur\",\n  \"serverFuncBtns\": \"Boutons de fonction du serveur\",\n  \"serverOrder\": \"Ordre du serveur\",\n  \"serverTabRequired\": \"L'onglet serveur ne peut pas être supprimé\",\n  \"sftpDlPrepare\": \"Préparation de la connexion...\",\n  \"sftpEditorTip\": \"Si vide, utilisez l’éditeur de fichiers intégré de l’application. Si une valeur est présente, utilisez l’éditeur du serveur distant, par exemple `vim` (il est recommandé de détecter automatiquement selon `EDITOR`).\",\n  \"sftpRmrDirSummary\": \"Utilisez `rm -r` pour supprimer un dossier en SFTP.\",\n  \"sftpSSHConnected\": \"SFTP Connecté\",\n  \"sftpShowFoldersFirst\": \"Afficher d'abord les dossiers\",\n  \"showDistLogo\": \"Afficher le logo de la distribution\",\n  \"size\": \"Taille\",\n  \"softWrap\": \"Retour à la ligne souple\",\n  \"specifyDev\": \"Spécifier l'appareil\",\n  \"specifyDevTip\": \"Par exemple, les statistiques de trafic réseau concernent par défaut tous les appareils. Vous pouvez spécifier ici un appareil particulier.\",\n  \"speed\": \"Vitesse\",\n  \"spentTime\": \"Temps écoulé : {time}\",\n  \"sshConfigAllExist\": \"Tous les serveurs existent déjà ({duplicateCount} doublons trouvés)\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} doublons seront ignorés\",\n  \"sshConfigFound\": \"Nous avons trouvé une configuration SSH sur votre système.\",\n  \"sshConfigFoundServers\": \"{totalCount} serveurs trouvés\",\n  \"sshConfigImport\": \"Importation de configuration SSH\",\n  \"sshConfigImportHelp\": \"Seules les informations de base peuvent être importées, par exemple : IP/Port.\",\n  \"sshConfigImportPermission\": \"Souhaitez-vous donner la permission de lire ~/.ssh/config et d'importer automatiquement les paramètres du serveur ?\",\n  \"sshConfigImportTip\": \"Proposer de lire ~/.ssh/config lors de la première création de serveur\",\n  \"sshConfigImported\": \"{count} serveurs importés depuis la configuration SSH\",\n  \"sshHostKeyChangedDesc\": \"La clé d'hôte SSH de {serverName} a changé. Ne continuez que si vous faites confiance à ce serveur.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Empreinte (MD5 Base64) : {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Empreinte (MD5 hex) : {fingerprint}\",\n  \"sshHostKeyType\": \"Type de clé d'hôte SSH\",\n  \"sshHostKeyNewDesc\": \"Une nouvelle clé d'hôte SSH a été reçue de {serverName}. Vérifiez l'empreinte avant de faire confiance.\",\n  \"sshHostKeyStoredFingerprint\": \"Empreinte enregistrée : {fingerprint}\",\n  \"sshConfigManualSelect\": \"Souhaitez-vous sélectionner manuellement le fichier de configuration SSH ?\",\n  \"sshConfigNoServers\": \"Aucun serveur trouvé dans la configuration SSH\",\n  \"sshConfigPermissionDenied\": \"Impossible d'accéder au fichier de configuration SSH en raison des permissions macOS.\",\n  \"sshConfigServersToImport\": \"{importCount} serveurs seront importés\",\n  \"sshTermHelp\": \"Lorsque le terminal est défilable, faire glisser horizontalement permet de sélectionner du texte. En cliquant sur le bouton du clavier, vous activez/désactivez le clavier. L'icône de fichier ouvre le chemin actuel SFTP. Le bouton du presse-papiers copie le contenu lorsque du texte est sélectionné, et colle le contenu du presse-papiers dans le terminal lorsqu'aucun texte n'est sélectionné et qu'il y a du contenu dans le presse-papiers. L'icône de code colle des extraits de code dans le terminal et les exécute.\",\n  \"sshTip\": \"Cette fonctionnalité est actuellement à l'étape expérimentale.\\n\\nVeuillez signaler les bugs sur {url} ou rejoindre notre développement.\",\n  \"sshVirtualKeyAutoOff\": \"Activation automatique des touches virtuelles\",\n  \"stat\": \"Statistiques\",\n  \"supportFmtArgs\": \"Les paramètres de mise en forme suivants sont pris en charge :\",\n  \"suspendTip\": \"La fonction de suspension nécessite des privilèges root et le support de systemd.\",\n  \"switchTo\": \"Passer à {val}\",\n  \"syncTip\": \"Un redémarrage peut être nécessaire pour que certains changements prennent effet.\",\n  \"system\": \"Système\",\n  \"tag\": \"Étiquettes\",\n  \"tapToStartDiscovery\": \"Appuyez sur le bouton de recherche pour découvrir les serveurs SSH sur votre réseau\",\n  \"termFontSizeTip\": \"Ce paramètre affectera la taille du terminal (largeur et hauteur). Vous pouvez zoomer sur la page du terminal pour ajuster la taille de la police de la session en cours.\",\n  \"textScaler\": \"Mise à l'échelle du texte\",\n  \"textScalerTip\": \"1.0 => 100% (taille originale), fonctionne uniquement sur la partie de la police de la page du serveur, il est déconseillé de la modifier.\",\n  \"time\": \"Temps\",\n  \"times\": \"Fois\",\n  \"trySudo\": \"Essayer d'utiliser sudo\",\n  \"unknown\": \"Inconnu\",\n  \"unkownConvertMode\": \"Mode de conversion inconnu\",\n  \"update\": \"Mettre à jour\",\n  \"updateIntervalEqual0\": \"Vous avez défini à 0, la mise à jour ne se fera pas automatiquement.\\nImpossible de calculer l'état du CPU.\",\n  \"updateServerStatusInterval\": \"Intervalle de mise à jour de l'état du serveur\",\n  \"upsideDown\": \"À l'envers\",\n  \"useCdn\": \"Utiliser CDN\",\n  \"useCdnTip\": \"Il est recommandé aux utilisateurs non chinois d'utiliser le CDN. Souhaitez-vous l'utiliser ?\",\n  \"useNoPwd\": \"Aucun mot de passe ne sera utilisé\",\n  \"usePodmanByDefault\": \"Par défaut avec Podman\",\n  \"used\": \"Utilisé\",\n  \"view\": \"Vue\",\n  \"viewDetails\": \"Voir les détails\",\n  \"viewErr\": \"Voir erreur\",\n  \"virtKeyHelpClipboard\": \"Copiez dans le presse-papiers si le terminal sélectionné n'est pas vide, sinon collez le contenu du presse-papiers dans le terminal.\",\n  \"virtKeyHelpIME\": \"Activer/désactiver le clavier\",\n  \"virtKeyHelpSFTP\": \"Ouvrir le répertoire actuel en SFTP.\",\n  \"waitConnection\": \"Veuillez attendre que la connexion soit établie.\",\n  \"wakeLock\": \"Maintenir éveillé\",\n  \"watchNotPaired\": \"Aucune Apple Watch associée\",\n  \"webdavSettingEmpty\": \"Le paramètre Webdav est vide\",\n  \"whenOpenApp\": \"À l'ouverture de l'application\",\n  \"wolTip\": \"Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.\",\n  \"write\": \"Écrire\",\n  \"writeScriptFailTip\": \"Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.\",\n  \"writeScriptTip\": \"Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \\n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script.\",\n  \"podmanDockerEmulationDetected\": \"Émulation Podman Docker détectée. Veuillez passer à Podman dans les paramètres.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_id.arb",
    "content": "{\n  \"@@locale\": \"id\",\n  \"aboutThanks\": \"Terima kasih kepada orang -orang berikut yang berpartisipasi.\",\n  \"acceptBeta\": \"Terima pembaruan versi uji coba\",\n  \"addSystemPrivateKeyTip\": \"Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?\",\n  \"added2List\": \"Ditambahkan ke Daftar Tugas\",\n  \"addr\": \"Alamat\",\n  \"alreadyLastDir\": \"Sudah di direktori terakhir.\",\n  \"askAi\": \"Tanya AI\",\n  \"askAiApiKey\": \"Kunci API\",\n  \"askAiAwaitingResponse\": \"Menunggu respons AI...\",\n  \"askAiBaseUrl\": \"URL dasar\",\n  \"askAiCommandInserted\": \"Perintah dimasukkan ke terminal\",\n  \"askAiConfigMissing\": \"Harap konfigurasikan {fields} di Pengaturan.\",\n  \"askAiConfirmExecute\": \"Konfirmasi sebelum menjalankan\",\n  \"askAiConversation\": \"Percakapan AI\",\n  \"askAiDisclaimer\": \"AI bisa saja salah. Gunakan dengan hati-hati.\",\n  \"askAiFollowUpHint\": \"Ajukan pertanyaan lanjutan...\",\n  \"askAiInsertTerminal\": \"Masukkan ke terminal\",\n  \"askAiNoResponse\": \"Tidak ada respons\",\n  \"askAiRecommendedCommand\": \"Perintah yang disarankan AI\",\n  \"askAiSelectedContent\": \"Konten yang dipilih\",\n  \"askAiUsageHint\": \"Digunakan di Terminal SSH\",\n  \"atLeastOneTab\": \"Setidaknya satu tab harus dipilih\",\n  \"authFailTip\": \"Otentikasi gagal, silakan periksa apakah kata sandi/kunci/host/pengguna, dll, salah.\",\n  \"autoBackupConflict\": \"Hanya satu pencadangan otomatis yang dapat diaktifkan pada saat yang bersamaan.\",\n  \"autoConnect\": \"Hubungkan otomatis\",\n  \"autoRun\": \"Berjalan Otomatis\",\n  \"autoUpdateHomeWidget\": \"Widget Rumah Pembaruan Otomatis\",\n  \"availableTabs\": \"Tab Tersedia\",\n  \"backupEncrypted\": \"Cadangan telah dienkripsi\",\n  \"backupNotEncrypted\": \"Cadangan tidak dienkripsi\",\n  \"backupPassword\": \"Kata sandi cadangan\",\n  \"backupPasswordRemoved\": \"Kata sandi cadangan dihapus\",\n  \"backupPasswordSet\": \"Kata sandi cadangan ditetapkan\",\n  \"backupPasswordTip\": \"Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.\",\n  \"backupPasswordWrong\": \"Kata sandi cadangan salah\",\n  \"backupTip\": \"Data yang diekspor dapat dienkripsi dengan kata sandi. \\nHarap jaga keamanannya.\",\n  \"backupVersionNotMatch\": \"Versi cadangan tidak cocok.\",\n  \"bgRun\": \"Jalankan di Backgroud\",\n  \"bgRunTip\": \"Sakelar ini hanya berarti aplikasi akan mencoba berjalan di latar belakang, apakah aplikasi dapat berjalan di latar belakang tergantung pada apakah izin diaktifkan atau tidak. Untuk Android asli, nonaktifkan \\\"Pengoptimalan Baterai\\\" di aplikasi ini, dan untuk miui, ubah kebijakan penghematan daya ke \\\"Tidak Terbatas\\\".\",\n  \"clearAllStatsContent\": \"Apakah Anda yakin ingin menghapus semua statistik koneksi server? Tindakan ini tidak dapat dibatalkan.\",\n  \"clearAllStatsTitle\": \"Hapus Semua Statistik\",\n  \"clearServerStatsContent\": \"Apakah Anda yakin ingin menghapus statistik koneksi untuk server \\\"{serverName}\\\"? Tindakan ini tidak dapat dibatalkan.\",\n  \"clearServerStatsTitle\": \"Hapus Statistik {serverName}\",\n  \"clearThisServerStats\": \"Hapus Statistik Server Ini\",\n  \"compactDatabase\": \"Kompres Database\",\n  \"compactDatabaseContent\": \"Ukuran database: {size}\\n\\nIni akan mengatur ulang database untuk mengurangi ukuran file. Tidak ada data yang akan dihapus.\",\n  \"closeAfterSave\": \"Simpan dan tutup\",\n  \"collapseUITip\": \"Apakah akan menciutkan daftar panjang yang ada di UI secara default atau tidak\",\n  \"connectionDetails\": \"Detail Koneksi\",\n  \"connectionStats\": \"Statistik Koneksi\",\n  \"connectionStatsDesc\": \"Lihat tingkat keberhasilan koneksi server dan riwayat\",\n  \"containerTrySudoTip\": \"Contohnya: Di dalam aplikasi, pengguna diatur sebagai aaa, tetapi Docker diinstal di bawah pengguna root. Dalam kasus ini, Anda perlu mengaktifkan opsi ini.\",\n  \"containerSudoPasswordRequired\": \"Kata sandi sudo diperlukan untuk mengakses Docker. Silakan masukkan kata sandi Anda.\",\n  \"containerSudoPasswordIncorrect\": \"Kata sandi sudo salah atau tidak diizinkan. Silakan coba lagi.\",\n  \"convert\": \"Mengubah\",\n  \"copyPath\": \"Path Copy\",\n  \"cpuViewAsProgressTip\": \"Tampilkan tingkat penggunaan setiap CPU dalam gaya bilah kemajuan (gaya lama)\",\n  \"cursorType\": \"Jenis kursor\",\n  \"customCmd\": \"Perintah kustom\",\n  \"customCmdHint\": \"\\\"Nama Perintah\\\": \\\"Perintah\\\"\",\n  \"deleteServers\": \"Penghapusan server secara batch\",\n  \"desktopTerminalTip\": \"Perintah yang digunakan untuk membuka emulator terminal saat memulai sesi SSH.\",\n  \"dirEmpty\": \"Pastikan dir kosong.\",\n  \"discoverSshServers\": \"Temukan Server SSH\",\n  \"discoveryFailed\": \"Penemuan gagal\",\n  \"discoverySettings\": \"Pengaturan Penemuan\",\n  \"discoverySummary\": \"Ringkasan Penemuan\",\n  \"diskHealth\": \"Kesehatan disk\",\n  \"diskIgnorePath\": \"Abaikan jalan untuk disk\",\n  \"displayCpuIndex\": \"Tampilkan indeks CPU\",\n  \"dl2Local\": \"Unduh {fileName} ke lokal?\",\n  \"dockerEmptyRunningItems\": \"Tidak ada wadah yang sedang berjalan.\\nHal ini dapat terjadi karena:\\n- Pengguna instalasi Docker tidak sama dengan nama pengguna yang dikonfigurasi di dalam Aplikasi.\\n- Variabel lingkungan DOCKER_HOST tidak terbaca dengan benar. Anda bisa mendapatkannya dengan menjalankan `echo $DOCKER_HOST` di terminal.\",\n  \"dockerImagesFmt\": \"{count} gambar\",\n  \"dockerNotInstalled\": \"Docker tidak terpasang\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} running, {stoppedCount} container stopped.\",\n  \"dockerStatusRunningFmt\": \"{count} wadah berjalan.\",\n  \"doubleColumnMode\": \"Mode kolom ganda\",\n  \"doubleColumnTip\": \"Opsi ini hanya mengaktifkan fitur, apakah itu benar-benar dapat diaktifkan tergantung pada lebar perangkat\",\n  \"editVirtKeys\": \"Edit kunci virtual\",\n  \"editorHighlightTip\": \"Performa penyorotan kode saat ini lebih buruk, dan dapat dimatikan secara opsional untuk perbaikan.\",\n  \"enableMdns\": \"Aktifkan mDNS\",\n  \"enableMdnsDesc\": \"Gunakan mDNS/Bonjour untuk menemukan layanan SSH\",\n  \"envVars\": \"Variabel lingkungan\",\n  \"experimentalFeature\": \"Fitur eksperimental\",\n  \"extraArgs\": \"Args ekstra\",\n  \"fallbackSshDest\": \"Tujuan SSH mundur\",\n  \"fdroidReleaseTip\": \"Jika Anda mengunduh aplikasi ini dari F-Droid, disarankan untuk mematikan opsi ini.\",\n  \"fgService\": \"Layanan Latar Depan\",\n  \"fgServiceTip\": \"Setelah diaktifkan, beberapa model perangkat mungkin crash. Menonaktifkannya dapat menyebabkan beberapa model tidak dapat mempertahankan koneksi SSH di latar belakang. Harap izinkan perizinan notifikasi ServerBox, menjalankan di latar belakang, dan bangun mandiri di pengaturan sistem.\",\n  \"fileTooLarge\": \"File '{file}' terlalu besar {size}, max {sizeMax}\",\n  \"finishedAt\": \"Selesai pada\",\n  \"followSystem\": \"Ikuti sistem\",\n  \"fontSize\": \"Ukuran huruf\",\n  \"fullScreen\": \"Mode Layar Penuh\",\n  \"fullScreenJitter\": \"Jitter layar penuh\",\n  \"fullScreenJitterHelp\": \"Untuk menghindari pembakaran layar\",\n  \"fullScreenTip\": \"Apakah mode layar penuh diaktifkan ketika perangkat diputar ke modus lanskap? Opsi ini hanya berlaku untuk tab server.\",\n  \"goBackQ\": \"Datang kembali?\",\n  \"goto\": \"Pergi ke\",\n  \"hideTitleBar\": \"Sembunyikan bilah judul\",\n  \"highlight\": \"Sorotan kode\",\n  \"homeTabs\": \"Tab Beranda\",\n  \"homeTabsCustomizeDesc\": \"Sesuaikan tab mana yang muncul di halaman beranda dan urutannya\",\n  \"homeWidgetUrlConfig\": \"Konfigurasi URL Widget Rumah\",\n  \"httpFailedWithCode\": \"Permintaan gagal, kode status: {code}\",\n  \"ignoreCert\": \"Abaikan sertifikat\",\n  \"image\": \"Gambar\",\n  \"imagesList\": \"Daftar gambar\",\n  \"installDockerWithUrl\": \"Silakan https://docs.docker.com/engine/install Docker pertama.\",\n  \"invalid\": \"Tidak valid\",\n  \"jumpServer\": \"Lompat server\",\n  \"keepForeground\": \"Simpan Aplikasi Foreground!\",\n  \"keepStatusWhenErr\": \"Menyimpan status server terakhir\",\n  \"keepStatusWhenErrTip\": \"Hanya ketika terjadi kesalahan saat menjalankan skrip\",\n  \"keyAuth\": \"Auth kunci\",\n  \"lastFailure\": \"Gagal Terakhir\",\n  \"lastSuccess\": \"Sukses Terakhir\",\n  \"letterCache\": \"Caching huruf\",\n  \"letterCacheTip\": \"Direkomendasikan untuk menonaktifkan, tetapi setelah dinonaktifkan, tidak mungkin untuk memasukkan karakter CJK.\",\n  \"madeWithLove\": \"Dibuat dengan ❤️ oleh {myGithub}\",\n  \"max\": \"Max\",\n  \"maxConcurrency\": \"Konkurensi Maksimum\",\n  \"maxRetryCount\": \"Jumlah penyambungan kembali server\",\n  \"maxRetryCountEqual0\": \"Akan mencoba lagi lagi dan lagi.\",\n  \"min\": \"Min\",\n  \"more\": \"Lebih Banyak\",\n  \"moveOutServerFuncBtnsHelp\": \"Aktif: dapat ditampilkan di bawah setiap kartu pada halaman Tab Server. Nonaktif: dapat ditampilkan di bagian atas halaman Rincian Server.\",\n  \"needHomeDir\": \"Jika Anda pengguna Synology, [lihat di sini](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Pengguna sistem lain perlu mencari cara membuat direktori home.\",\n  \"needRestart\": \"Perlu memulai ulang aplikasi\",\n  \"netViewType\": \"Jenis tampilan bersih\",\n  \"newContainer\": \"Wadah baru\",\n  \"noConnectionStatsData\": \"Tidak ada data statistik koneksi\",\n  \"noLineChart\": \"Jangan gunakan grafik garis\",\n  \"noLineChartForCpu\": \"Jangan gunakan diagram garis untuk CPU\",\n  \"noPrivateKeyTip\": \"Kunci privat tidak ada, mungkin telah dihapus atau ada kesalahan konfigurasi.\",\n  \"noPromptAgain\": \"Jangan tanya lagi\",\n  \"onServerDetailPage\": \"Di halaman detail server\",\n  \"onlyOneLine\": \"Hanya tampilkan sebagai satu baris (dapat digulir)\",\n  \"onlyWhenCoreBiggerThan8\": \"Berlaku hanya ketika jumlah inti > 8\",\n  \"openLastPath\": \"Buka jalur terakhir\",\n  \"openLastPathTip\": \"Server yang berbeda akan memiliki catatan yang berbeda, dan catatan tersebut adalah jalur menuju pintu keluar\",\n  \"parseContainerStatsTip\": \"Parsing status okupansi oleh Docker agak lambat\",\n  \"percentOfSize\": \"{percent}% dari {size}\",\n  \"permission\": \"Izin\",\n  \"pingInputIP\": \"Harap masukkan IP / domain target.\",\n  \"pingNoServer\": \"Tidak ada server untuk melakukan ping.\\nHarap tambahkan server di tab Server.\",\n  \"plugInType\": \"Jenis Penyisipan\",\n  \"preferDiskAmount\": \"Prioritaskan tampilan kapasitas disk\",\n  \"privateKey\": \"Kunci Pribadi\",\n  \"privateKeyNotFoundFmt\": \"Kunci privat [{keyId}] tidak ditemukan.\",\n  \"pushToken\": \"Dorong token\",\n  \"pveIgnoreCertTip\": \"Tidak disarankan untuk diaktifkan, waspadai risiko keamanan! Jika Anda menggunakan sertifikat default dari PVE, Anda perlu mengaktifkan opsi ini.\",\n  \"pveLoginFailed\": \"Login gagal. Tidak dapat mengautentikasi dengan nama pengguna/kata sandi dari konfigurasi server untuk login Linux PAM.\",\n  \"pveVersionLow\": \"Fitur ini saat ini sedang dalam tahap pengujian dan hanya diuji pada PVE 8+. Gunakan dengan hati-hati.\",\n  \"read\": \"Baca\",\n  \"recentConnections\": \"Koneksi Terkini\",\n  \"rememberPwdInMem\": \"Ingat kata sandi di dalam memori\",\n  \"rememberPwdInMemTip\": \"Digunakan untuk kontainer, menangguhkan, dll.\",\n  \"rememberWindowSize\": \"Ingat ukuran jendela\",\n  \"remotePath\": \"Jalur jarak jauh\",\n  \"result\": \"Hasil\",\n  \"rotateAngel\": \"Sudut rotasi\",\n  \"sameIdServerExist\": \"Server dengan ID yang sama sudah ada\",\n  \"save\": \"Menyimpan\",\n  \"second\": \"S\",\n  \"serverDetailOrder\": \"Detail pesanan widget halaman\",\n  \"serverFuncBtns\": \"Tombol fungsi server\",\n  \"serverOrder\": \"Pesanan server\",\n  \"serverTabRequired\": \"Tab server tidak dapat dihapus\",\n  \"sftpDlPrepare\": \"Bersiap untuk terhubung ...\",\n  \"sftpEditorTip\": \"Jika kosong, gunakan editor file bawaan aplikasi. Jika ada nilai, gunakan editor server jarak jauh, misalnya `vim` (disarankan untuk mendeteksi secara otomatis sesuai `EDITOR`).\",\n  \"sftpRmrDirSummary\": \"Gunakan `rm -r` untuk menghapus dir di SFTP\",\n  \"sftpSSHConnected\": \"Sftp terhubung\",\n  \"sftpShowFoldersFirst\": \"Folder ditampilkan lebih dulu\",\n  \"showDistLogo\": \"Tampilkan logo distribusi\",\n  \"size\": \"Ukuran\",\n  \"softWrap\": \"Pembungkus lembut\",\n  \"specifyDev\": \"Tentukan perangkat\",\n  \"specifyDevTip\": \"Misalnya, statistik lalu lintas jaringan secara default adalah untuk semua perangkat. Anda dapat menentukan perangkat tertentu di sini.\",\n  \"speed\": \"Kecepatan\",\n  \"spentTime\": \"Menghabiskan waktu: {time}\",\n  \"sshConfigAllExist\": \"Semua server sudah ada (ditemukan {duplicateCount} duplikat)\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} duplikat akan dilewati\",\n  \"sshConfigFound\": \"Kami menemukan konfigurasi SSH di sistem Anda\",\n  \"sshConfigFoundServers\": \"Ditemukan {totalCount} server\",\n  \"sshConfigImport\": \"Impor Konfigurasi SSH\",\n  \"sshConfigImportHelp\": \"Hanya informasi dasar yang dapat diimpor, misalnya: IP/Port.\",\n  \"sshConfigImportPermission\": \"Apakah Anda ingin memberikan izin untuk membaca ~/.ssh/config dan secara otomatis mengimpor pengaturan server?\",\n  \"sshConfigImportTip\": \"Prompt untuk membaca ~/.ssh/config saat pembuatan server pertama\",\n  \"sshConfigImported\": \"Berhasil mengimpor {count} server dari konfigurasi SSH\",\n  \"sshHostKeyChangedDesc\": \"Kunci host SSH untuk {serverName} telah berubah. Lanjutkan hanya jika Anda mempercayai server ini.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Sidik jari (MD5 Base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Sidik jari (MD5 hex): {fingerprint}\",\n  \"sshHostKeyType\": \"Jenis kunci host SSH\",\n  \"sshHostKeyNewDesc\": \"Kunci host SSH baru diterima dari {serverName}. Periksa sidik jarinya sebelum mempercayai.\",\n  \"sshHostKeyStoredFingerprint\": \"Sidik jari tersimpan: {fingerprint}\",\n  \"sshConfigManualSelect\": \"Apakah Anda ingin memilih file konfigurasi SSH secara manual?\",\n  \"sshConfigNoServers\": \"Tidak ada server yang ditemukan dalam konfigurasi SSH\",\n  \"sshConfigPermissionDenied\": \"Tidak dapat mengakses file konfigurasi SSH karena izin macOS.\",\n  \"sshConfigServersToImport\": \"{importCount} server akan diimpor\",\n  \"sshTermHelp\": \"Ketika terminal dapat digulirkan, menggeser secara horizontal dapat memilih teks. Mengklik tombol keyboard mengaktifkan/menonaktifkan keyboard. Ikon file membuka SFTP jalur saat ini. Tombol papan klip menyalin konten saat teks dipilih, dan menempelkan konten dari papan klip ke terminal saat tidak ada teks yang dipilih dan ada konten di papan klip. Ikon kode menempelkan potongan kode ke terminal dan mengeksekusinya.\",\n  \"sshTip\": \"Fungsi ini sekarang dalam tahap eksperimen.\\n\\nHarap laporkan bug di {url} atau bergabunglah dengan pengembangan kami.\",\n  \"sshVirtualKeyAutoOff\": \"Switching Otomatis Kunci Virtual\",\n  \"stat\": \"Statistik\",\n  \"supportFmtArgs\": \"Parameter pemformatan berikut ini didukung:\",\n  \"suspendTip\": \"Fungsi penangguhan memerlukan hak akses root dan dukungan systemd.\",\n  \"switchTo\": \"Beralih ke {val}\",\n  \"syncTip\": \"Pengaktifan ulang mungkin diperlukan agar beberapa perubahan dapat diterapkan.\",\n  \"system\": \"Sistem\",\n  \"tag\": \"Tag\",\n  \"tapToStartDiscovery\": \"Tekan tombol pencarian untuk menemukan server SSH di jaringan Anda\",\n  \"termFontSizeTip\": \"Pengaturan ini akan memengaruhi ukuran terminal (lebar dan tinggi). Anda dapat melakukan zoom pada halaman terminal untuk menyesuaikan ukuran font sesi saat ini.\",\n  \"textScaler\": \"Penskalaan font\",\n  \"textScalerTip\": \"1.0 => 100% (ukuran asli), hanya berfungsi pada bagian halaman server font, tidak disarankan untuk diubah.\",\n  \"time\": \"Waktu\",\n  \"times\": \"Waktu\",\n  \"trySudo\": \"Cobalah menggunakan sudo\",\n  \"unknown\": \"Tidak dikenal\",\n  \"unkownConvertMode\": \"Mode Konversi Tidak Diketahui\",\n  \"update\": \"Memperbarui\",\n  \"updateIntervalEqual0\": \"Anda mengatur ke 0, tidak akan memperbarui secara otomatis.\\nTidak dapat menghitung status CPU.\",\n  \"updateServerStatusInterval\": \"Interval Pembaruan Status Server\",\n  \"upsideDown\": \"Terbalik\",\n  \"useCdn\": \"Menggunakan CDN\",\n  \"useCdnTip\": \"Pengguna non-Cina disarankan menggunakan CDN. Apakah Anda ingin menggunakannya?\",\n  \"useNoPwd\": \"Tidak ada kata sandi yang akan digunakan\",\n  \"usePodmanByDefault\": \"Menggunakan Podman sebagai bawaan\",\n  \"used\": \"Digunakan\",\n  \"view\": \"Tampilan\",\n  \"viewDetails\": \"Lihat Detail\",\n  \"viewErr\": \"Lihat kesalahan\",\n  \"virtKeyHelpClipboard\": \"Salin ke clipboard jika terminal yang dipilih tidak kosong, jika tidak, tempel isi clipboard ke terminal.\",\n  \"virtKeyHelpIME\": \"Menyalakan/mematikan keyboard\",\n  \"virtKeyHelpSFTP\": \"Buka direktori saat ini di SFTP.\",\n  \"waitConnection\": \"Harap tunggu koneksi akan dibuat.\",\n  \"wakeLock\": \"Tetap terjaga\",\n  \"watchNotPaired\": \"Tidak ada Apple Watch yang dipasangkan\",\n  \"webdavSettingEmpty\": \"Pengaturan webdav kosong\",\n  \"whenOpenApp\": \"Saat membuka aplikasi\",\n  \"wolTip\": \"Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.\",\n  \"write\": \"Tulis\",\n  \"writeScriptFailTip\": \"Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.\",\n  \"writeScriptTip\": \"Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \\n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.\",\n  \"podmanDockerEmulationDetected\": \"Emulasi Podman Docker terdeteksi. Silakan beralih ke Podman di pengaturan.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_it.arb",
    "content": "{\n  \"@@locale\": \"it\",\n  \"aboutThanks\": \"Grazie alle seguenti persone che hanno partecipato.\",\n  \"acceptBeta\": \"Accetta aggiornamenti versione beta\",\n  \"addSystemPrivateKeyTip\": \"Attualmente non esistono chiavi private, vuoi aggiungere quella fornita dal sistema (~/.ssh/id_rsa)?\",\n  \"added2List\": \"Aggiunto alla lista delle attività\",\n  \"addr\": \"Indirizzo\",\n  \"alreadyLastDir\": \"Già nell'ultima directory.\",\n  \"askAi\": \"Chiedi all'IA\",\n  \"askAiApiKey\": \"Chiave API\",\n  \"askAiAwaitingResponse\": \"In attesa della risposta dell'IA...\",\n  \"askAiBaseUrl\": \"URL base\",\n  \"askAiCommandInserted\": \"Comando inserito nel terminale\",\n  \"askAiConfigMissing\": \"Configura {fields} in Impostazioni.\",\n  \"askAiConfirmExecute\": \"Conferma prima di eseguire\",\n  \"askAiConversation\": \"Conversazione IA\",\n  \"askAiDisclaimer\": \"L'IA potrebbe essere errata. Rivedi attentamente prima di applicare.\",\n  \"askAiFollowUpHint\": \"Fai una domanda di follow-up...\",\n  \"askAiInsertTerminal\": \"Inserisci nel terminale\",\n  \"askAiNoResponse\": \"Nessuna risposta\",\n  \"askAiRecommendedCommand\": \"Comando suggerito dall'IA\",\n  \"askAiSelectedContent\": \"Contenuto selezionato\",\n  \"askAiUsageHint\": \"Utilizzato nel Terminale SSH\",\n  \"atLeastOneTab\": \"Deve essere selezionata almeno una scheda\",\n  \"authFailTip\": \"Autenticazione fallita, verifica se le credenziali sono corrette\",\n  \"autoBackupConflict\": \"Solo un backup automatico può essere attivato alla volta.\",\n  \"autoConnect\": \"Connessione automatica\",\n  \"autoRun\": \"Esecuzione automatica\",\n  \"autoUpdateHomeWidget\": \"Aggiornamento automatico widget home\",\n  \"availableTabs\": \"Schede disponibili\",\n  \"backupEncrypted\": \"Il backup è crittografato\",\n  \"backupNotEncrypted\": \"Il backup non è crittografato\",\n  \"backupPassword\": \"Password di backup\",\n  \"backupPasswordRemoved\": \"Password di backup rimossa\",\n  \"backupPasswordSet\": \"Password di backup impostata\",\n  \"backupPasswordTip\": \"Imposta una password per crittografare i file di backup. Lascia vuoto per disabilitare la crittografia.\",\n  \"backupPasswordWrong\": \"Password di backup errata\",\n  \"backupTip\": \"I dati esportati possono essere crittografati con password.\\nConservali al sicuro.\",\n  \"backupVersionNotMatch\": \"La versione del backup non corrisponde.\",\n  \"bgRun\": \"Esegui in background\",\n  \"bgRunTip\": \"Questa opzione significa solo che il programma cercherà di eseguire in background. Se può eseguire in background dipende dal fatto che il permesso sia abilitato o meno. Per le ROM Android basate su AOSP, disabilita \\\"Ottimizzazione batteria\\\" in questa app. Per MIUI/HyperOS, cambia la politica di risparmio energetico su \\\"Illimitato\\\".\",\n  \"clearAllStatsContent\": \"Sei sicuro di voler cancellare tutte le statistiche di connessione del server? Questa azione non può essere annullata.\",\n  \"clearAllStatsTitle\": \"Cancella tutte le statistiche\",\n  \"clearServerStatsContent\": \"Sei sicuro di voler cancellare le statistiche di connessione per il server \\\"{serverName}\\\"? Questa azione non può essere annullata.\",\n  \"clearServerStatsTitle\": \"Cancella statistiche {serverName}\",\n  \"clearThisServerStats\": \"Cancella statistiche di questo server\",\n  \"compactDatabase\": \"Compatta database\",\n  \"compactDatabaseContent\": \"Dimensione database: {size}\\n\\nQuesto riorganizzerà il database per ridurre la dimensione del file. Nessun dato verrà eliminato.\",\n  \"closeAfterSave\": \"Salva e chiudi\",\n  \"collapseUITip\": \"Se comprimere le liste lunghe presenti nell'interfaccia utente per impostazione predefinita\",\n  \"connectionDetails\": \"Dettagli connessione\",\n  \"connectionStats\": \"Statistiche connessione\",\n  \"connectionStatsDesc\": \"Visualizza il tasso di successo della connessione al server e la cronologia\",\n  \"containerTrySudoTip\": \"Ad esempio: nell'app, l'utente è impostato su aaa, ma Docker è installato sotto l'utente root. In questo caso, devi abilitare questa opzione.\",\n  \"containerSudoPasswordRequired\": \"È richiesta la password sudo per accedere a Docker. Inserisci la tua password.\",\n  \"containerSudoPasswordIncorrect\": \"La password sudo è errata o non consentita. Riprova.\",\n  \"convert\": \"Converti\",\n  \"copyPath\": \"Copia percorso\",\n  \"cpuViewAsProgressTip\": \"Visualizza l'utilizzo di ogni CPU in stile barra di avanzamento (stile vecchio)\",\n  \"cursorType\": \"Tipo di cursore\",\n  \"customCmd\": \"Comandi personalizzati\",\n  \"customCmdHint\": \"\\\"Nome comando\\\": \\\"Comando\\\"\",\n  \"deleteServers\": \"Elimina server in blocco\",\n  \"desktopTerminalTip\": \"Comando utilizzato per aprire l'emulatore di terminale quando si avviano sessioni SSH.\",\n  \"dirEmpty\": \"Assicurati che la cartella sia vuota.\",\n  \"discoverSshServers\": \"Scopri server SSH\",\n  \"discoveryFailed\": \"Scoperta fallita\",\n  \"discoverySettings\": \"Impostazioni scoperta\",\n  \"discoverySummary\": \"Riepilogo scoperta\",\n  \"diskHealth\": \"Salute disco\",\n  \"diskIgnorePath\": \"Ignora percorso per disco\",\n  \"displayCpuIndex\": \"Mostra indice CPU\",\n  \"dl2Local\": \"Scaricare {fileName} in locale?\",\n  \"dockerEmptyRunningItems\": \"Non ci sono container in esecuzione.\\nQuesto potrebbe essere perché:\\n- L'utente di installazione di Docker non è lo stesso del nome utente configurato nell'App.\\n- La variabile d'ambiente DOCKER_HOST non è stata letta correttamente. Puoi ottenerla eseguendo `echo $DOCKER_HOST` nel terminale.\",\n  \"dockerImagesFmt\": \"{count} immagini\",\n  \"dockerNotInstalled\": \"Docker non installato\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} in esecuzione, {stoppedCount} container fermati.\",\n  \"dockerStatusRunningFmt\": \"{count} container in esecuzione.\",\n  \"doubleColumnMode\": \"Modalità a doppia colonna\",\n  \"doubleColumnTip\": \"Questa opzione abilita solo la funzione, se può essere effettivamente abilitata dipende dalla larghezza del dispositivo\",\n  \"editVirtKeys\": \"Modifica tasti virtuali\",\n  \"editorHighlightTip\": \"Le attuali prestazioni di evidenziazione del codice non sono ideali e possono essere disabilitate opzionalmente per migliorare.\",\n  \"enableMdns\": \"Abilita mDNS\",\n  \"enableMdnsDesc\": \"Usa mDNS/Bonjour per scoprire servizi SSH\",\n  \"envVars\": \"Variabile d'ambiente\",\n  \"experimentalFeature\": \"Funzionalità sperimentale\",\n  \"extraArgs\": \"Argomenti extra\",\n  \"fallbackSshDest\": \"Destinazione SSH di fallback\",\n  \"fdroidReleaseTip\": \"Se hai scaricato questa app da F-Droid, si consiglia di disattivare questa opzione.\",\n  \"fgService\": \"Servizio in primo piano\",\n  \"fgServiceTip\": \"Dopo l'attivazione, alcuni modelli di dispositivo potrebbero arrestarsi in modo anomalo. Disabilitarlo potrebbe causare l'impossibilità per alcuni modelli di mantenere le connessioni SSH in background. Consenti le autorizzazioni di notifica ServerBox, l'esecuzione in background e l'auto-riattivazione nelle impostazioni di sistema.\",\n  \"fileTooLarge\": \"File '{file}' troppo grande {size}, max {sizeMax}\",\n  \"finishedAt\": \"Completato alle\",\n  \"followSystem\": \"Segui sistema\",\n  \"fontSize\": \"Dimensione carattere\",\n  \"fullScreen\": \"Modalità schermo intero\",\n  \"fullScreenJitter\": \"Jitter schermo intero\",\n  \"fullScreenJitterHelp\": \"Per evitare il burn-in dello schermo\",\n  \"fullScreenTip\": \"La modalità a schermo intero deve essere abilitata quando il dispositivo viene ruotato in modalità orizzontale? Questa opzione si applica solo alla scheda server.\",\n  \"goBackQ\": \"Tornare indietro?\",\n  \"goto\": \"Vai a\",\n  \"hideTitleBar\": \"Nascondi barra del titolo\",\n  \"highlight\": \"Evidenziazione codice\",\n  \"homeTabs\": \"Schede home\",\n  \"homeTabsCustomizeDesc\": \"Personalizza quali schede appaiono nella home page e il loro ordine\",\n  \"homeWidgetUrlConfig\": \"Configura url widget home\",\n  \"httpFailedWithCode\": \"richiesta fallita, codice stato: {code}\",\n  \"ignoreCert\": \"Ignora certificato\",\n  \"image\": \"Immagine\",\n  \"imagesList\": \"Elenco immagini\",\n  \"installDockerWithUrl\": \"Installa prima docker da https://docs.docker.com/engine/install .\",\n  \"invalid\": \"Non valido\",\n  \"invalidHostFormat\": \"Formato host non valido. Sono consentiti solo caratteri IPv4, IPv6 e di dominio.\",\n  \"jumpServer\": \"Server di salto\",\n  \"keepForeground\": \"Mantieni l'app in primo piano!\",\n  \"keepStatusWhenErr\": \"Conserva l'ultimo stato del server\",\n  \"keepStatusWhenErrTip\": \"Solo in caso di errore durante l'esecuzione dello script\",\n  \"keyAuth\": \"Autenticazione chiave\",\n  \"lastFailure\": \"Ultimo fallimento\",\n  \"lastSuccess\": \"Ultimo successo\",\n  \"letterCache\": \"Cache lettere\",\n  \"letterCacheTip\": \"Si consiglia di disabilitare, ma dopo aver disabilitato, non sarà possibile inserire caratteri CJK.\",\n  \"madeWithLove\": \"Realizzato con ❤️ da {myGithub}\",\n  \"max\": \"max\",\n  \"maxConcurrency\": \"Massima concorrenza\",\n  \"maxRetryCount\": \"Numero di riconnessioni del server\",\n  \"maxRetryCountEqual0\": \"Proverà di nuovo e ancora.\",\n  \"min\": \"min\",\n  \"more\": \"Altro\",\n  \"moveOutServerFuncBtnsHelp\": \"Attivo: può essere visualizzato sotto ogni carta nella pagina Scheda Server. Disattivato: può essere visualizzato nella parte superiore della pagina Dettagli Server.\",\n  \"needHomeDir\": \"Se sei un utente Synology, [vedi qui](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Gli utenti di altri sistemi devono cercare come creare una directory home.\",\n  \"needRestart\": \"L'app deve essere riavviata\",\n  \"netViewType\": \"Tipo di visualizzazione rete\",\n  \"newContainer\": \"Nuovo container\",\n  \"noConnectionStatsData\": \"Nessun dato di statistiche di connessione\",\n  \"noLineChart\": \"Non usare grafici a linee\",\n  \"noLineChartForCpu\": \"Non usare grafici a linee per la CPU\",\n  \"noPrivateKeyTip\": \"La chiave privata non esiste, potrebbe essere stata eliminata o c'è un errore di configurazione.\",\n  \"noPromptAgain\": \"Non chiedere di nuovo\",\n  \"onServerDetailPage\": \"Nella pagina dettagli server\",\n  \"onlyOneLine\": \"Visualizza solo come una riga (scorrevole)\",\n  \"onlyWhenCoreBiggerThan8\": \"Funziona solo quando il numero di core è maggiore di 8\",\n  \"openLastPath\": \"Apri l'ultimo percorso\",\n  \"openLastPathTip\": \"Server diversi avranno log diversi e il log è il percorso di uscita\",\n  \"parseContainerStatsTip\": \"L'analisi dello stato di occupazione di Docker è relativamente lenta.\",\n  \"percentOfSize\": \"{percent}% di {size}\",\n  \"permission\": \"Permessi\",\n  \"pingInputIP\": \"Inserisci un IP / dominio di destinazione.\",\n  \"pingNoServer\": \"Nessun server da pingare.\\nAggiungi un server nella scheda server.\",\n  \"plugInType\": \"Tipo di inserimento\",\n  \"preferDiskAmount\": \"Priorità visualizzazione capacità disco\",\n  \"privateKey\": \"Chiave privata\",\n  \"privateKeyNotFoundFmt\": \"Chiave privata [{keyId}] non trovata.\",\n  \"pushToken\": \"Token push\",\n  \"pveIgnoreCertTip\": \"Non si consiglia di abilitare, attento ai rischi per la sicurezza! Se stai usando il certificato predefinito da PVE, devi abilitare questa opzione.\",\n  \"pveLoginFailed\": \"Accesso fallito. Impossibile autenticarsi con nome utente/password dalla configurazione del server per l'accesso Linux PAM.\",\n  \"pveVersionLow\": \"Questa funzionalità è attualmente nella fase di test ed è stata testata solo su PVE 8+. Usala con cautela.\",\n  \"read\": \"Leggi\",\n  \"recentConnections\": \"Connessioni recenti\",\n  \"rememberPwdInMem\": \"Ricorda password in memoria\",\n  \"rememberPwdInMemTip\": \"Utilizzato per container, sospensione, ecc.\",\n  \"rememberWindowSize\": \"Ricorda dimensione finestra\",\n  \"remotePath\": \"Percorso remoto\",\n  \"result\": \"Risultato\",\n  \"rotateAngel\": \"Angolo di rotazione\",\n  \"sameIdServerExist\": \"Esiste già un server con lo stesso ID\",\n  \"save\": \"Salva\",\n  \"second\": \"s\",\n  \"serverDetailOrder\": \"Ordine widget pagina dettagli\",\n  \"serverFuncBtns\": \"Pulsanti funzione server\",\n  \"serverOrder\": \"Ordine server\",\n  \"serverTabRequired\": \"La scheda server non può essere rimossa\",\n  \"sftpDlPrepare\": \"Preparazione alla connessione...\",\n  \"sftpEditorTip\": \"Se vuoto, usa l'editor di file integrato dell'app. Se è presente un valore, usa l'editor del server remoto, ad es. `vim` (si consiglia di rilevare automaticamente secondo `EDITOR`).\",\n  \"sftpRmrDirSummary\": \"Usa `rm -r` per eliminare una cartella in SFTP.\",\n  \"sftpSSHConnected\": \"SFTP connesso\",\n  \"sftpShowFoldersFirst\": \"Mostra prima le cartelle\",\n  \"showDistLogo\": \"Mostra logo distribuzione\",\n  \"size\": \"Dimensione\",\n  \"softWrap\": \"A capo automatico\",\n  \"specifyDev\": \"Specifica dispositivo\",\n  \"specifyDevTip\": \"Ad esempio, le statistiche del traffico di rete sono per impostazione predefinita per tutti i dispositivi. Puoi specificare un dispositivo particolare qui.\",\n  \"speed\": \"Velocità\",\n  \"spentTime\": \"Tempo impiegato: {time}\",\n  \"sshConfigAllExist\": \"Tutti i server esistono già ({duplicateCount} duplicati trovati)\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} duplicati verranno saltati\",\n  \"sshConfigFound\": \"Abbiamo trovato la configurazione SSH sul tuo sistema.\",\n  \"sshConfigFoundServers\": \"Trovati {totalCount} server\",\n  \"sshConfigImport\": \"Importa configurazione SSH\",\n  \"sshConfigImportHelp\": \"Solo le informazioni di base possono essere importate, ad esempio: IP/Porta.\",\n  \"sshConfigImportPermission\": \"Vuoi dare il permesso di leggere ~/.ssh/config e importare automaticamente le impostazioni del server?\",\n  \"sshConfigImportTip\": \"Chiedi di leggere ~/.ssh/config alla prima creazione del server\",\n  \"sshConfigImported\": \"Importati {count} server dalla configurazione SSH\",\n  \"sshHostKeyChangedDesc\": \"La chiave host SSH è cambiata per {serverName}. Continua solo se ti fidi di questo server.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Impronta digitale (MD5 base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Impronta digitale (MD5 hex): {fingerprint}\",\n  \"sshHostKeyType\": \"Tipo chiave host SSH\",\n  \"@sshHostKeyType\": {\n    \"description\": \"Etichetta per il tipo di chiave host SSH visualizzata nella finestra di dialogo di verifica della chiave host.\"\n  },\n  \"sshHostKeyNewDesc\": \"È stata ricevuta una nuova chiave host SSH da {serverName}. Rivedi l'impronta digitale prima di fidarti.\",\n  \"sshHostKeyStoredFingerprint\": \"Impronta digitale memorizzata: {fingerprint}\",\n  \"sshConfigManualSelect\": \"Vuoi selezionare manualmente il file di configurazione SSH?\",\n  \"sshConfigNoServers\": \"Nessun server trovato nella configurazione SSH\",\n  \"sshConfigPermissionDenied\": \"Impossibile accedere al file di configurazione SSH a causa dei permessi macOS.\",\n  \"sshConfigServersToImport\": \"{importCount} server verranno importati\",\n  \"sshTermHelp\": \"Quando il terminale è scorrevole, trascinare orizzontalmente può selezionare il testo. Cliccando il pulsante tastiera accende/spegne la tastiera. L'icona file apre il percorso corrente SFTP. Il pulsante appunti copia il contenuto quando il testo è selezionato e incolla il contenuto dagli appunti nel terminale quando nessun testo è selezionato e c'è contenuto negli appunti. L'icona codice incolla snippet di codice nel terminale ed esegue.\",\n  \"sshTip\": \"Questa funzione è ora nella fase sperimentale.\\n\\nSegnala i bug su {url} o unisciti al nostro sviluppo.\",\n  \"sshVirtualKeyAutoOff\": \"Commutazione automatica dei tasti virtuali\",\n  \"stat\": \"Statistiche\",\n  \"supportFmtArgs\": \"Sono supportati i seguenti parametri di formattazione:\",\n  \"suspendTip\": \"La funzione di sospensione richiede il permesso root e il supporto systemd.\",\n  \"switchTo\": \"Passa a {val}\",\n  \"syncTip\": \"Potrebbe essere necessario un riavvio affinché alcune modifiche abbiano effetto.\",\n  \"system\": \"Sistema\",\n  \"tag\": \"Tag\",\n  \"tapToStartDiscovery\": \"Tocca il pulsante di ricerca per scoprire i server SSH sulla tua rete\",\n  \"termFontSizeTip\": \"Questa impostazione influirà sulla dimensione del terminale (larghezza e altezza). Puoi ingrandire la pagina del terminale per regolare la dimensione del carattere della sessione corrente.\",\n  \"textScaler\": \"Scalatore testo\",\n  \"textScalerTip\": \"1.0 => 100% (dimensione originale), funziona solo su parte del carattere della pagina server, non si consiglia di cambiare.\",\n  \"time\": \"Tempo\",\n  \"times\": \"Volte\",\n  \"trySudo\": \"Prova a usare sudo\",\n  \"unknown\": \"Sconosciuto\",\n  \"unkownConvertMode\": \"Modalità di conversione sconosciuta\",\n  \"update\": \"Aggiorna\",\n  \"updateIntervalEqual0\": \"Hai impostato a 0, non aggiornerà automaticamente.\\nNon può calcolare lo stato della CPU.\",\n  \"updateServerStatusInterval\": \"Intervallo di aggiornamento stato server\",\n  \"upsideDown\": \"Capovolto\",\n  \"useCdn\": \"Utilizzo CDN\",\n  \"useCdnTip\": \"Si consiglia agli utenti non cinesi di usare CDN. Vuoi usarlo?\",\n  \"useNoPwd\": \"Non verrà usata nessuna password\",\n  \"usePodmanByDefault\": \"Usa Podman per impostazione predefinita\",\n  \"used\": \"Usato\",\n  \"view\": \"Visualizza\",\n  \"viewDetails\": \"Visualizza dettagli\",\n  \"viewErr\": \"Vedi errore\",\n  \"virtKeyHelpClipboard\": \"Copia negli appunti se il terminale selezionato non è vuoto, altrimenti incolla il contenuto degli appunti nel terminale.\",\n  \"virtKeyHelpIME\": \"Accendi/spegni la tastiera\",\n  \"virtKeyHelpSFTP\": \"Apri la directory corrente in SFTP.\",\n  \"waitConnection\": \"Attendi che la connessione venga stabilita.\",\n  \"wakeLock\": \"Mantieni sveglio\",\n  \"watchNotPaired\": \"Nessun Apple Watch associato\",\n  \"webdavSettingEmpty\": \"Impostazione WebDav vuota\",\n  \"whenOpenApp\": \"All'apertura dell'app\",\n  \"wolTip\": \"Dopo aver configurato WOL (Wake-on-LAN), viene inviata una richiesta WOL ogni volta che il server è connesso.\",\n  \"write\": \"Scrivi\",\n  \"writeScriptFailTip\": \"Scrittura dello script fallita, forse a causa di mancanza di permessi o la directory non esiste.\",\n  \"writeScriptTip\": \"Dopo essersi connessi al server, uno script verrà scritto in `~/.config/server_box` \\n | `/tmp/server_box` per monitorare lo stato del sistema. Puoi rivedere il contenuto dello script.\",\n  \"menuGitHubRepository\": \"Repository GitHub\",\n  \"podmanDockerEmulationDetected\": \"Rilevata emulazione Docker Podman. Passa a Podman nelle impostazioni.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_ja.arb",
    "content": "{\n  \"@@locale\": \"ja\",\n  \"aboutThanks\": \"以下の参加者に感謝します。\",\n  \"acceptBeta\": \"テストバージョンの更新を受け入れる\",\n  \"addSystemPrivateKeyTip\": \"現在秘密鍵がありません。システムのデフォルト(~/.ssh/id_rsa)を追加しますか？\",\n  \"added2List\": \"タスクリストに追加されました\",\n  \"addr\": \"アドレス\",\n  \"alreadyLastDir\": \"すでに最上位のディレクトリです\",\n  \"askAi\": \"AI に質問\",\n  \"askAiApiKey\": \"API キー\",\n  \"askAiAwaitingResponse\": \"AI の応答を待機中...\",\n  \"askAiBaseUrl\": \"ベース URL\",\n  \"askAiCommandInserted\": \"コマンドをターミナルに挿入しました\",\n  \"askAiConfigMissing\": \"設定で {fields} を構成してください。\",\n  \"askAiConfirmExecute\": \"実行前に確認\",\n  \"askAiConversation\": \"AI 会話\",\n  \"askAiDisclaimer\": \"AI が誤る可能性があります。注意してご利用ください。\",\n  \"askAiFollowUpHint\": \"追質問をする...\",\n  \"askAiInsertTerminal\": \"ターミナルに挿入\",\n  \"askAiNoResponse\": \"応答なし\",\n  \"askAiRecommendedCommand\": \"AI 推奨コマンド\",\n  \"askAiSelectedContent\": \"選択した内容\",\n  \"askAiUsageHint\": \"SSH ターミナルで使用\",\n  \"atLeastOneTab\": \"少なくとも1つのタブを選択する必要があります\",\n  \"authFailTip\": \"認証に失敗しました。パスワード/鍵/ホスト/ユーザーなどが間違っていないか確認してください。\",\n  \"autoBackupConflict\": \"自動バックアップは一度に一つしか開始できません\",\n  \"autoConnect\": \"自動接続\",\n  \"autoRun\": \"自動実行\",\n  \"autoUpdateHomeWidget\": \"ホームウィジェットを自動更新\",\n  \"availableTabs\": \"利用可能なタブ\",\n  \"backupEncrypted\": \"バックアップは暗号化されています\",\n  \"backupNotEncrypted\": \"バックアップは暗号化されていません\",\n  \"backupPassword\": \"バックアップパスワード\",\n  \"backupPasswordRemoved\": \"バックアップパスワードが削除されました\",\n  \"backupPasswordSet\": \"バックアップパスワードが設定されました\",\n  \"backupPasswordTip\": \"バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。\",\n  \"backupPasswordWrong\": \"バックアップパスワードが間違っています\",\n  \"backupTip\": \"エクスポートされたデータはパスワードで暗号化できます。 \\n適切に保管してください。\",\n  \"backupVersionNotMatch\": \"バックアップバージョンが一致しないため、復元できません\",\n  \"bgRun\": \"バックグラウンド実行\",\n  \"bgRunTip\": \"このスイッチはプログラムがバックグラウンドで実行を試みることを意味しますが、実際にバックグラウンドで実行できるかどうかは、権限が有効になっているかに依存します。AOSPベースのAndroid ROMでは、このアプリの「バッテリー最適化」をオフにしてください。MIUIでは、省エネモードを「無制限」に変更してください。\",\n  \"clearAllStatsContent\": \"すべてのサーバー接続統計を削除してもよろしいですか？この操作は元に戻せません。\",\n  \"clearAllStatsTitle\": \"すべての統計をクリア\",\n  \"clearServerStatsContent\": \"サーバー\\\"{serverName}\\\"の接続統計を削除してもよろしいですか？この操作は元に戻せません。\",\n  \"clearServerStatsTitle\": \"{serverName}の統計をクリア\",\n  \"clearThisServerStats\": \"このサーバーの統計をクリア\",\n  \"compactDatabase\": \"データベースを圧縮\",\n  \"compactDatabaseContent\": \"データベースサイズ: {size}\\n\\nこれにより、ファイルサイズを小さくするためにデータベースが再編成されます。データは削除されません。\",\n  \"closeAfterSave\": \"保存して閉じる\",\n  \"collapseUITip\": \"UIの長いリストをデフォルトで折りたたむかどうか\",\n  \"connectionDetails\": \"接続の詳細\",\n  \"connectionStats\": \"接続統計\",\n  \"connectionStatsDesc\": \"サーバー接続成功率と履歴を表示\",\n  \"containerTrySudoTip\": \"例：アプリ内でユーザーをaaaに設定しているが、Dockerがrootユーザーでインストールされている場合、このオプションを有効にする必要があります\",\n  \"containerSudoPasswordRequired\": \"Dockerにアクセスするにはsudoパスワードが必要です。パスワードを入力してください。\",\n  \"containerSudoPasswordIncorrect\": \"sudoパスワードが正しくないか、許可されていません。再試行してください。\",\n  \"convert\": \"変換\",\n  \"copyPath\": \"パスをコピー\",\n  \"cpuViewAsProgressTip\": \"各CPUの使用率をプログレスバースタイルで表示する（旧スタイル）\",\n  \"cursorType\": \"カーソルタイプ\",\n  \"customCmd\": \"カスタムコマンド\",\n  \"customCmdHint\": \"\\\"コマンド名\\\": \\\"コマンド\\\"\",\n  \"deleteServers\": \"サーバーを一括削除\",\n  \"desktopTerminalTip\": \"SSHセッションを起動する際に使用されるターミナルエミュレーターを開くコマンド。\",\n  \"dirEmpty\": \"フォルダーが空であることを確認してください\",\n  \"discoverSshServers\": \"SSHサーバーの発見\",\n  \"discoveryFailed\": \"発見に失敗\",\n  \"discoverySettings\": \"発見設定\",\n  \"discoverySummary\": \"発見の概要\",\n  \"diskHealth\": \"ディスクの健康状態\",\n  \"diskIgnorePath\": \"無視されたディスクパス\",\n  \"displayCpuIndex\": \"CPUインデックスを表示する\",\n  \"dl2Local\": \"{fileName}をローカルにダウンロードしますか？\",\n  \"dockerEmptyRunningItems\": \"実行中のコンテナがありません。\\nこれは次の理由による可能性があります：\\n- Dockerのインストールユーザーとアプリ内の設定されたユーザー名が異なる\\n- 環境変数DOCKER_HOSTが正しく読み込まれていない。ターミナルで`echo $DOCKER_HOST`を実行して取得できます。\",\n  \"dockerImagesFmt\": \"合計{count}イメージ\",\n  \"dockerNotInstalled\": \"Dockerがインストールされていません\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount}個が実行中、{stoppedCount}個が停止中\",\n  \"dockerStatusRunningFmt\": \"{count}個のコンテナが実行中\",\n  \"doubleColumnMode\": \"ダブルカラムモード\",\n  \"doubleColumnTip\": \"このオプションは機能を有効にするだけで、実際に有効にできるかどうかはデバイスの幅に依存します\",\n  \"editVirtKeys\": \"仮想キーを編集\",\n  \"editorHighlightTip\": \"現在のコードハイライトのパフォーマンスはかなり悪いため、改善するために無効にすることを選択できます。\",\n  \"enableMdns\": \"mDNSを有効化\",\n  \"enableMdnsDesc\": \"mDNS/BonjourでSSHサービスを発見\",\n  \"envVars\": \"環境変数\",\n  \"experimentalFeature\": \"実験的な機能\",\n  \"extraArgs\": \"追加引数\",\n  \"fallbackSshDest\": \"フォールバックSSH宛先\",\n  \"fdroidReleaseTip\": \"このアプリをF-Droidからダウンロードした場合、このオプションをオフにすることをお勧めします。\",\n  \"fgService\": \"フォアグラウンドサービス\",\n  \"fgServiceTip\": \"有効にすると、一部の機種でクラッシュする可能性があります。無効にすると、一部の機種でバックグラウンドでのSSH接続を維持できなくなる可能性があります。システム設定でServerBoxの通知権限、バックグラウンド実行、自己起動を許可してください。\",\n  \"fileTooLarge\": \"ファイル '{file}' は大きすぎます '{size}'、{sizeMax} を超えています\",\n  \"finishedAt\": \"完了時刻\",\n  \"followSystem\": \"システムに従う\",\n  \"fontSize\": \"フォントサイズ\",\n  \"fullScreen\": \"フルスクリーンモード\",\n  \"fullScreenJitter\": \"フルスクリーンモードのジッター\",\n  \"fullScreenJitterHelp\": \"焼き付き防止\",\n  \"fullScreenTip\": \"デバイスが横向きに回転したときにフルスクリーンモードを有効にしますか？このオプションはサーバータブにのみ適用されます。\",\n  \"goBackQ\": \"戻りますか？\",\n  \"goto\": \"移動\",\n  \"hideTitleBar\": \"タイトルバーを非表示にする\",\n  \"highlight\": \"コードハイライト\",\n  \"homeTabs\": \"ホームタブ\",\n  \"homeTabsCustomizeDesc\": \"ホームページに表示するタブとその順序をカスタマイズします\",\n  \"homeWidgetUrlConfig\": \"ホームウィジェットURL設定\",\n  \"httpFailedWithCode\": \"リクエスト失敗、ステータスコード: {code}\",\n  \"ignoreCert\": \"証明書を無視する\",\n  \"image\": \"イメージ\",\n  \"imagesList\": \"イメージリスト\",\n  \"installDockerWithUrl\": \"最初に https://docs.docker.com/engine/install dockerをインストールしてください\",\n  \"invalid\": \"無効\",\n  \"jumpServer\": \"ジャンプサーバー\",\n  \"keepForeground\": \"アプリを前面に保ってください！\",\n  \"keepStatusWhenErr\": \"エラー時に前回のサーバーステータスを保持\",\n  \"keepStatusWhenErrTip\": \"スクリプトの実行エラーに限ります\",\n  \"keyAuth\": \"キー認証\",\n  \"lastFailure\": \"最後の失敗\",\n  \"lastSuccess\": \"最後の成功\",\n  \"letterCache\": \"文字キャッシング\",\n  \"letterCacheTip\": \"無効にすることを推奨しますが、無効にした後はCJK文字を入力することができなくなります。\",\n  \"madeWithLove\": \"{myGithub}によって❤️で作成済み\",\n  \"max\": \"最大\",\n  \"maxConcurrency\": \"最大同時実行数\",\n  \"maxRetryCount\": \"サーバーの再接続試行回数\",\n  \"maxRetryCountEqual0\": \"無限に再試行します\",\n  \"min\": \"最小\",\n  \"more\": \"もっと\",\n  \"moveOutServerFuncBtnsHelp\": \"有効にする：サーバータブの各カードの下に表示されます。無効にする：サーバーの詳細ページの上部に表示されます。\",\n  \"needHomeDir\": \"Synologyユーザーの場合は、[こちらをご覧ください](https://kb.synology.com/DSM/tutorial/user_enable_home_service)。他のシステムのユーザーは、ホームディレクトリの作成方法を検索する必要があります。\",\n  \"needRestart\": \"アプリを再起動する必要があります\",\n  \"netViewType\": \"ネットワークビュータイプ\",\n  \"newContainer\": \"新しいコンテナを作成\",\n  \"noConnectionStatsData\": \"接続統計データがありません\",\n  \"noLineChart\": \"折れ線グラフを使用しない\",\n  \"noLineChartForCpu\": \"CPUに折れ線グラフを使わない\",\n  \"noPrivateKeyTip\": \"秘密鍵が存在しません。削除されたか、設定ミスがある可能性があります。\",\n  \"noPromptAgain\": \"再度確認しない\",\n  \"onServerDetailPage\": \"サーバーの詳細ページで\",\n  \"onlyOneLine\": \"一行のみ表示（スクロール可能）\",\n  \"onlyWhenCoreBiggerThan8\": \"コア数が8より大きい場合にのみ有効\",\n  \"openLastPath\": \"最後のパスを開く\",\n  \"openLastPathTip\": \"異なるサーバーには異なる記録があり、記録されているのは退出時のパスです\",\n  \"parseContainerStatsTip\": \"Dockerの使用状況の解析は比較的遅いです\",\n  \"percentOfSize\": \"{size} の {percent}%\",\n  \"permission\": \"権限\",\n  \"pingInputIP\": \"対象のIPまたはドメインを入力してください\",\n  \"pingNoServer\": \"Pingに使用するサーバーがありません\\nサーバータブでサーバーを追加してから再試行してください\",\n  \"plugInType\": \"挿入タイプ\",\n  \"preferDiskAmount\": \"ディスク容量を優先的に表示\",\n  \"privateKey\": \"秘密鍵\",\n  \"privateKeyNotFoundFmt\": \"秘密鍵 [{keyId}] が見つかりません。\",\n  \"pushToken\": \"プッシュトークン\",\n  \"pveIgnoreCertTip\": \"オプションを有効にすることは推奨されません、セキュリティリスクに注意してください！PVEのデフォルト証明書を使用している場合は、このオプションを有効にする必要があります。\",\n  \"pveLoginFailed\": \"ログインに失敗しました。Linux PAMログインのためにサーバー構成からのユーザー名/パスワードで認証できません。\",\n  \"pveVersionLow\": \"この機能は現在テスト段階にあり、PVE 8+でのみテストされています。ご利用の際は慎重に。\",\n  \"read\": \"読み取り\",\n  \"recentConnections\": \"最近の接続\",\n  \"rememberPwdInMem\": \"メモリにパスワードを記憶する\",\n  \"rememberPwdInMemTip\": \"コンテナ、一時停止などに使用されます。\",\n  \"rememberWindowSize\": \"ウィンドウサイズを記憶する\",\n  \"remotePath\": \"リモートパス\",\n  \"result\": \"結果\",\n  \"rotateAngel\": \"回転角度\",\n  \"sameIdServerExist\": \"同じIDのサーバーが既に存在します\",\n  \"save\": \"保存\",\n  \"second\": \"秒\",\n  \"serverDetailOrder\": \"詳細ページのウィジェット順序\",\n  \"serverFuncBtns\": \"サーバー機能ボタン\",\n  \"serverOrder\": \"サーバー順序\",\n  \"serverTabRequired\": \"サーバータブは削除できません\",\n  \"sftpDlPrepare\": \"サーバーへの接続を準備中...\",\n  \"sftpEditorTip\": \"空の場合は、アプリ内蔵のファイルエディタを使用します。値がある場合は、リモートサーバーのエディタ（例：`vim`）を使用します（`EDITOR` に従って自動検出することをお勧めします）。\",\n  \"sftpRmrDirSummary\": \"SFTPで`rm -r`を使用してフォルダーを削除\",\n  \"sftpSSHConnected\": \"SFTPに接続されました...\",\n  \"sftpShowFoldersFirst\": \"フォルダーを先に表示\",\n  \"showDistLogo\": \"ディストリビューションのロゴを表示\",\n  \"size\": \"サイズ\",\n  \"softWrap\": \"ソフトラップ\",\n  \"specifyDev\": \"デバイスを指定\",\n  \"specifyDevTip\": \"例えば、ネットワークトラフィック統計はデフォルトですべてのデバイスに対するものです。ここで特定のデバイスを指定できます。\",\n  \"speed\": \"速度\",\n  \"spentTime\": \"費した時間: {time}\",\n  \"sshConfigAllExist\": \"すべてのサーバーがすでに存在します（{duplicateCount}個の重複が見つかりました）\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount}個の重複がスキップされます\",\n  \"sshConfigFound\": \"システムにSSH設定が見つかりました。\",\n  \"sshConfigFoundServers\": \"{totalCount}個のサーバーが見つかりました\",\n  \"sshConfigImport\": \"SSH設定のインポート\",\n  \"sshConfigImportHelp\": \"インポートできるのは基本情報のみです。例：IP/ポート。\",\n  \"sshConfigImportPermission\": \"~/.ssh/configを読み取ってサーバー設定を自動的にインポートする権限を与えますか？\",\n  \"sshConfigImportTip\": \"初回サーバー作成時に~/.ssh/configの読み取りを促す\",\n  \"sshConfigImported\": \"SSH設定から{count}個のサーバーをインポートしました\",\n  \"sshHostKeyChangedDesc\": \"{serverName} の SSH ホスト鍵が変更されました。このサーバーを信頼できる場合のみ続行してください。\",\n  \"sshHostKeyFingerprintMd5Base64\": \"フィンガープリント (MD5 Base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"フィンガープリント (MD5 16進): {fingerprint}\",\n  \"sshHostKeyType\": \"SSH ホストキーの種類\",\n  \"sshHostKeyNewDesc\": \"{serverName} から新しい SSH ホスト鍵を受信しました。信頼する前にフィンガープリントを確認してください。\",\n  \"sshHostKeyStoredFingerprint\": \"保存済みフィンガープリント: {fingerprint}\",\n  \"sshConfigManualSelect\": \"SSH設定ファイルを手動で選択しますか？\",\n  \"sshConfigNoServers\": \"SSH設定でサーバーが見つかりませんでした\",\n  \"sshConfigPermissionDenied\": \"macOSの権限により、SSH設定ファイルにアクセスできません。\",\n  \"sshConfigServersToImport\": \"{importCount}個のサーバーがインポートされます\",\n  \"sshTermHelp\": \"ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。\",\n  \"sshTip\": \"この機能は現在テスト段階にあります。\\n\\n問題がある場合は、{url}でフィードバックしてください。\",\n  \"sshVirtualKeyAutoOff\": \"仮想キーの自動オフ\",\n  \"stat\": \"統計\",\n  \"supportFmtArgs\": \"以下のフォーマット引数がサポートされています：\",\n  \"suspendTip\": \"suspend機能はroot権限とsystemdのサポートが必要です。\",\n  \"switchTo\": \"{val}に切り替える\",\n  \"syncTip\": \"再起動が必要な場合があります。一部の変更はその後に有効になります。\",\n  \"system\": \"システム\",\n  \"tag\": \"タグ\",\n  \"tapToStartDiscovery\": \"検索ボタンをタップしてネットワーク上のSSHサーバーを発見\",\n  \"termFontSizeTip\": \"この設定は端末のサイズ（幅と高さ）に影響します。現在のセッションのフォントサイズを調整するために、端末ページを拡大縮小できます。\",\n  \"textScaler\": \"テキストスケーラー\",\n  \"textScalerTip\": \"1.0 => 100%（デフォルトサイズ）。サーバーページの一部のテキストにのみ適用されます。変更をお勧めしません。\",\n  \"time\": \"時間\",\n  \"times\": \"回\",\n  \"trySudo\": \"sudoを試みる\",\n  \"unknown\": \"不明\",\n  \"unkownConvertMode\": \"未知の変換モード\",\n  \"update\": \"更新\",\n  \"updateIntervalEqual0\": \"0に設定すると、サーバーの状態は自動的に更新されず、CPU使用率も計算できません。\",\n  \"updateServerStatusInterval\": \"サーバー状態の更新間隔\",\n  \"upsideDown\": \"上下逆転\",\n  \"useCdn\": \"CDNの使用\",\n  \"useCdnTip\": \"中国以外のユーザーにはCDNの使用が推奨されています。ご利用しますか？\",\n  \"useNoPwd\": \"パスワードなしで使用します\",\n  \"usePodmanByDefault\": \"デフォルトでPodmanを使用\",\n  \"used\": \"使用済み\",\n  \"view\": \"ビュー\",\n  \"viewDetails\": \"詳細を表示\",\n  \"viewErr\": \"エラーを表示\",\n  \"virtKeyHelpClipboard\": \"端末に選択された文字がある場合は、選択された文字をクリップボードにコピーします。そうでない場合は、クリップボードの内容を端末に貼り付けます。\",\n  \"virtKeyHelpIME\": \"キーボードのオン/オフ\",\n  \"virtKeyHelpSFTP\": \"現在のパスでSFTPを開く。\",\n  \"waitConnection\": \"接続の確立を待ってください\",\n  \"wakeLock\": \"起動を保つ\",\n  \"watchNotPaired\": \"ペアリングされたApple Watchがありません\",\n  \"webdavSettingEmpty\": \"Webdavの設定が空です\",\n  \"whenOpenApp\": \"アプリを開くとき\",\n  \"wolTip\": \"WOL（Wake-on-LAN）を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。\",\n  \"write\": \"書き込み\",\n  \"writeScriptFailTip\": \"スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。\",\n  \"writeScriptTip\": \"サーバーへの接続後、システムステータスを監視するスクリプトが `~/.config/server_box` \\n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。\",\n  \"podmanDockerEmulationDetected\": \"Podman Docker エミュレーションが検出されました。設定で Podman に切り替えてください。\"\n}\n"
  },
  {
    "path": "lib/l10n/app_ko.arb",
    "content": "{\n  \"@@locale\": \"ko\",\n  \"aboutThanks\": \"다음 참여자분들에게 감사드립니다.\",\n  \"acceptBeta\": \"베타 버전 업데이트 수락\",\n  \"addSystemPrivateKeyTip\": \"현재 개인 키가 없습니다. 시스템 기본 키(~/.ssh/id_rsa)를 추가하시겠습니까?\",\n  \"added2List\": \"작업 목록에 추가되었습니다\",\n  \"addr\": \"주소\",\n  \"alreadyLastDir\": \"이미 마지막 디렉토리입니다.\",\n  \"askAi\": \"AI에게 질문\",\n  \"askAiApiKey\": \"API 키\",\n  \"askAiAwaitingResponse\": \"AI 응답 대기 중...\",\n  \"askAiBaseUrl\": \"기본 URL\",\n  \"askAiCommandInserted\": \"명령어가 터미널에 삽입되었습니다\",\n  \"askAiConfigMissing\": \"설정에서 {fields}을(를) 구성해 주세요.\",\n  \"askAiConfirmExecute\": \"실행 전 확인\",\n  \"askAiConversation\": \"AI 대화\",\n  \"askAiDisclaimer\": \"AI가 부정확할 수 있습니다. 적용 전에 주의 깊게 검토해 주세요.\",\n  \"askAiFollowUpHint\": \"추가 질문하기...\",\n  \"askAiInsertTerminal\": \"터미널에 삽입\",\n  \"askAiNoResponse\": \"응답 없음\",\n  \"askAiRecommendedCommand\": \"AI 추천 명령어\",\n  \"askAiSelectedContent\": \"선택한 내용\",\n  \"askAiUsageHint\": \"SSH 터미널에서 사용\",\n  \"atLeastOneTab\": \"최소 하나의 탭을 선택해야 합니다\",\n  \"authFailTip\": \"인증에 실패했습니다. 자격 증명이 올바른지 확인해 주세요.\",\n  \"autoBackupConflict\": \"자동 백업은 한 번에 하나만 활성화할 수 있습니다.\",\n  \"autoConnect\": \"자동 연결\",\n  \"autoRun\": \"자동 실행\",\n  \"autoUpdateHomeWidget\": \"홈 위젯 자동 업데이트\",\n  \"availableTabs\": \"사용 가능한 탭\",\n  \"backupEncrypted\": \"백업이 암호화되어 있습니다\",\n  \"backupNotEncrypted\": \"백업이 암호화되어 있지 않습니다\",\n  \"backupPassword\": \"백업 비밀번호\",\n  \"backupPasswordRemoved\": \"백업 비밀번호가 제거되었습니다\",\n  \"backupPasswordSet\": \"백업 비밀번호가 설정되었습니다\",\n  \"backupPasswordTip\": \"백업 파일을 암호화하기 위한 비밀번호를 설정하세요. 암호화를 비활성화하려면 비워 두세요.\",\n  \"backupPasswordWrong\": \"백업 비밀번호가 올바르지 않습니다\",\n  \"backupTip\": \"내보낸 데이터는 비밀번호로 암호화할 수 있습니다.\\n안전하게 보관해 주세요.\",\n  \"backupVersionNotMatch\": \"백업 버전이 일치하지 않습니다.\",\n  \"bgRun\": \"백그라운드 실행\",\n  \"bgRunTip\": \"이 스위치는 프로그램이 백그라운드에서 실행을 시도한다는 의미입니다. 실제 백그라운드 실행 가능 여부는 권한 활성화 여부에 따라 다릅니다. AOSP 기반 Android ROM의 경우, 이 앱의 \\\"배터리 최적화\\\"를 비활성화해 주세요. MIUI / HyperOS의 경우, 절전 정책을 \\\"무제한\\\"으로 변경해 주세요.\",\n  \"clearAllStatsContent\": \"모든 서버 연결 통계를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\",\n  \"clearAllStatsTitle\": \"모든 통계 삭제\",\n  \"clearServerStatsContent\": \"서버 \\\"{serverName}\\\"의 연결 통계를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\",\n  \"clearServerStatsTitle\": \"{serverName} 통계 삭제\",\n  \"clearThisServerStats\": \"이 서버의 통계 삭제\",\n  \"compactDatabase\": \"데이터베이스 압축\",\n  \"compactDatabaseContent\": \"데이터베이스 크기: {size}\\n\\n데이터베이스를 재구성하여 파일 크기를 줄입니다. 데이터는 삭제되지 않습니다.\",\n  \"closeAfterSave\": \"저장 후 닫기\",\n  \"collapseUITip\": \"UI의 긴 목록을 기본적으로 접을지 여부\",\n  \"connectionDetails\": \"연결 상세 정보\",\n  \"connectionStats\": \"연결 통계\",\n  \"connectionStatsDesc\": \"서버 연결 성공률 및 기록 보기\",\n  \"containerTrySudoTip\": \"예: 앱에서 사용자를 aaa로 설정했지만 Docker가 root 사용자로 설치된 경우, 이 옵션을 활성화해야 합니다.\",\n  \"containerSudoPasswordRequired\": \"Docker에 접근하려면 sudo 비밀번호가 필요합니다. 비밀번호를 입력해 주세요.\",\n  \"containerSudoPasswordIncorrect\": \"sudo 비밀번호가 올바르지 않거나 허용되지 않습니다. 다시 시도해 주세요.\",\n  \"convert\": \"변환\",\n  \"copyPath\": \"경로 복사\",\n  \"cpuViewAsProgressTip\": \"각 CPU 사용률을 프로그레스 바 형태로 표시합니다 (이전 스타일)\",\n  \"cursorType\": \"커서 유형\",\n  \"customCmd\": \"사용자 정의 명령어\",\n  \"customCmdHint\": \"\\\"명령어 이름\\\": \\\"명령어\\\"\",\n  \"deleteServers\": \"서버 일괄 삭제\",\n  \"desktopTerminalTip\": \"SSH 세션을 시작할 때 사용할 터미널 에뮬레이터를 여는 명령어입니다.\",\n  \"dirEmpty\": \"폴더가 비어 있는지 확인해 주세요.\",\n  \"discoverSshServers\": \"SSH 서버 검색\",\n  \"discoveryFailed\": \"검색 실패\",\n  \"discoverySettings\": \"검색 설정\",\n  \"discoverySummary\": \"검색 요약\",\n  \"diskHealth\": \"디스크 상태\",\n  \"diskIgnorePath\": \"디스크 무시 경로\",\n  \"displayCpuIndex\": \"CPU 인덱스 표시\",\n  \"dl2Local\": \"{fileName}을(를) 로컬에 다운로드하시겠습니까?\",\n  \"dockerEmptyRunningItems\": \"실행 중인 컨테이너가 없습니다.\\n다음과 같은 원인이 있을 수 있습니다:\\n- Docker 설치 사용자와 앱에 설정된 사용자 이름이 다릅니다.\\n- 환경 변수 DOCKER_HOST가 올바르게 읽히지 않았습니다. 터미널에서 `echo $DOCKER_HOST`를 실행하여 확인할 수 있습니다.\",\n  \"dockerImagesFmt\": \"이미지 {count}개\",\n  \"dockerNotInstalled\": \"Docker가 설치되어 있지 않습니다\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount}개 실행 중, {stoppedCount}개 중지됨.\",\n  \"dockerStatusRunningFmt\": \"컨테이너 {count}개 실행 중.\",\n  \"doubleColumnMode\": \"이중 열 모드\",\n  \"doubleColumnTip\": \"이 옵션은 기능만 활성화하며, 실제 적용 여부는 기기의 너비에 따라 다릅니다\",\n  \"editVirtKeys\": \"가상 키 편집\",\n  \"editorHighlightTip\": \"현재 코드 하이라이팅 성능이 이상적이지 않습니다. 성능 향상을 위해 선택적으로 끌 수 있습니다.\",\n  \"enableMdns\": \"mDNS 활성화\",\n  \"enableMdnsDesc\": \"mDNS/Bonjour를 사용하여 SSH 서비스 검색\",\n  \"envVars\": \"환경 변수\",\n  \"experimentalFeature\": \"실험적 기능\",\n  \"extraArgs\": \"추가 인수\",\n  \"fallbackSshDest\": \"대체 SSH 대상\",\n  \"fdroidReleaseTip\": \"F-Droid에서 이 앱을 다운로드한 경우, 이 옵션을 끄는 것을 권장합니다.\",\n  \"fgService\": \"포그라운드 서비스\",\n  \"fgServiceTip\": \"활성화 시 일부 기기에서 충돌이 발생할 수 있습니다. 비활성화 시 일부 기기에서 백그라운드 SSH 연결을 유지할 수 없을 수 있습니다. 시스템 설정에서 ServerBox 알림 권한, 백그라운드 실행, 자체 깨우기를 허용해 주세요.\",\n  \"fileTooLarge\": \"파일 '{file}'이(가) 너무 큽니다. 크기 {size}, 최대 {sizeMax}\",\n  \"finishedAt\": \"완료 시각\",\n  \"followSystem\": \"시스템 설정 따르기\",\n  \"fontSize\": \"글꼴 크기\",\n  \"fullScreen\": \"전체 화면 모드\",\n  \"fullScreenJitter\": \"전체 화면 지터\",\n  \"fullScreenJitterHelp\": \"화면 번인 방지\",\n  \"fullScreenTip\": \"기기를 가로 모드로 회전할 때 전체 화면 모드를 활성화하시겠습니까? 이 옵션은 서버 탭에만 적용됩니다.\",\n  \"goBackQ\": \"돌아가시겠습니까?\",\n  \"goto\": \"이동\",\n  \"hideTitleBar\": \"제목 표시줄 숨기기\",\n  \"highlight\": \"코드 하이라이팅\",\n  \"homeTabs\": \"홈 탭\",\n  \"homeTabsCustomizeDesc\": \"홈 페이지에 표시할 탭과 순서를 사용자 지정합니다\",\n  \"homeWidgetUrlConfig\": \"홈 위젯 URL 설정\",\n  \"httpFailedWithCode\": \"요청 실패, 상태 코드: {code}\",\n  \"ignoreCert\": \"인증서 무시\",\n  \"image\": \"이미지\",\n  \"imagesList\": \"이미지 목록\",\n  \"installDockerWithUrl\": \"먼저 https://docs.docker.com/engine/install 에서 Docker를 설치해 주세요.\",\n  \"invalid\": \"유효하지 않음\",\n  \"invalidHostFormat\": \"잘못된 호스트 형식입니다. IPv4, IPv6, 도메인 문자만 허용됩니다.\",\n  \"jumpServer\": \"점프 서버\",\n  \"keepForeground\": \"앱을 포그라운드에 유지해 주세요!\",\n  \"keepStatusWhenErr\": \"마지막 서버 상태 유지\",\n  \"keepStatusWhenErrTip\": \"스크립트 실행 중 오류가 발생한 경우에만\",\n  \"keyAuth\": \"키 인증\",\n  \"lastFailure\": \"마지막 실패\",\n  \"lastSuccess\": \"마지막 성공\",\n  \"letterCache\": \"문자 캐싱\",\n  \"letterCacheTip\": \"비활성화를 권장하지만, 비활성화하면 CJK 문자를 입력할 수 없습니다.\",\n  \"madeWithLove\": \"{myGithub}이(가) ❤️로 만들었습니다\",\n  \"max\": \"최대\",\n  \"maxConcurrency\": \"최대 동시 실행 수\",\n  \"maxRetryCount\": \"서버 재연결 횟수\",\n  \"maxRetryCountEqual0\": \"계속해서 재시도합니다.\",\n  \"min\": \"최소\",\n  \"more\": \"더 보기\",\n  \"moveOutServerFuncBtnsHelp\": \"켜기: 서버 탭의 각 카드 아래에 표시됩니다. 끄기: 서버 상세 페이지 상단에 표시됩니다.\",\n  \"needHomeDir\": \"Synology 사용자는 [여기를 참조하세요](https://kb.synology.com/DSM/tutorial/user_enable_home_service). 다른 시스템 사용자는 홈 디렉토리 생성 방법을 검색해야 합니다.\",\n  \"needRestart\": \"앱을 다시 시작해야 합니다\",\n  \"netViewType\": \"네트워크 뷰 유형\",\n  \"newContainer\": \"새 컨테이너\",\n  \"noConnectionStatsData\": \"연결 통계 데이터가 없습니다\",\n  \"noLineChart\": \"꺾은선 그래프 사용 안 함\",\n  \"noLineChartForCpu\": \"CPU에 꺾은선 그래프 사용 안 함\",\n  \"noPrivateKeyTip\": \"개인 키가 존재하지 않습니다. 삭제되었거나 설정 오류일 수 있습니다.\",\n  \"noPromptAgain\": \"다시 묻지 않기\",\n  \"onServerDetailPage\": \"서버 상세 페이지에서\",\n  \"onlyOneLine\": \"한 줄로만 표시 (스크롤 가능)\",\n  \"onlyWhenCoreBiggerThan8\": \"코어 수가 8개 이상일 때만 작동합니다\",\n  \"openLastPath\": \"마지막 경로 열기\",\n  \"openLastPathTip\": \"서버마다 다른 기록이 있으며, 기록은 종료 시의 경로입니다\",\n  \"parseContainerStatsTip\": \"Docker 점유 상태 파싱이 비교적 느립니다.\",\n  \"percentOfSize\": \"{size}의 {percent}%\",\n  \"permission\": \"권한\",\n  \"pingInputIP\": \"대상 IP / 도메인을 입력해 주세요.\",\n  \"pingNoServer\": \"Ping할 서버가 없습니다.\\n서버 탭에서 서버를 추가해 주세요.\",\n  \"plugInType\": \"삽입 유형\",\n  \"preferDiskAmount\": \"디스크 용량 우선 표시\",\n  \"privateKey\": \"개인 키\",\n  \"privateKeyNotFoundFmt\": \"개인 키 [{keyId}]을(를) 찾을 수 없습니다.\",\n  \"pushToken\": \"푸시 토큰\",\n  \"pveIgnoreCertTip\": \"활성화를 권장하지 않습니다. 보안 위험에 주의하세요! PVE 기본 인증서를 사용하는 경우 이 옵션을 활성화해야 합니다.\",\n  \"pveLoginFailed\": \"로그인에 실패했습니다. 서버 설정의 사용자 이름/비밀번호로 Linux PAM 인증을 할 수 없습니다.\",\n  \"pveVersionLow\": \"이 기능은 현재 테스트 단계이며 PVE 8+에서만 테스트되었습니다. 주의하여 사용해 주세요.\",\n  \"read\": \"읽기\",\n  \"recentConnections\": \"최근 연결\",\n  \"rememberPwdInMem\": \"메모리에 비밀번호 저장\",\n  \"rememberPwdInMemTip\": \"컨테이너, 일시 중지 등에 사용됩니다.\",\n  \"rememberWindowSize\": \"창 크기 기억\",\n  \"remotePath\": \"원격 경로\",\n  \"result\": \"결과\",\n  \"rotateAngel\": \"회전 각도\",\n  \"sameIdServerExist\": \"동일한 ID의 서버가 이미 존재합니다\",\n  \"save\": \"저장\",\n  \"second\": \"초\",\n  \"serverDetailOrder\": \"상세 페이지 위젯 순서\",\n  \"serverFuncBtns\": \"서버 기능 버튼\",\n  \"serverOrder\": \"서버 순서\",\n  \"serverTabRequired\": \"서버 탭은 제거할 수 없습니다\",\n  \"sftpDlPrepare\": \"연결 준비 중...\",\n  \"sftpEditorTip\": \"비어 있으면 앱 내장 파일 편집기를 사용합니다. 값이 있으면 원격 서버의 편집기(예: `vim`)를 사용합니다 (`EDITOR`에 따라 자동 감지 권장).\",\n  \"sftpRmrDirSummary\": \"SFTP에서 `rm -r`을 사용하여 폴더를 삭제합니다.\",\n  \"sftpSSHConnected\": \"SFTP 연결됨\",\n  \"sftpShowFoldersFirst\": \"폴더 우선 표시\",\n  \"showDistLogo\": \"배포판 로고 표시\",\n  \"size\": \"크기\",\n  \"softWrap\": \"자동 줄바꿈\",\n  \"specifyDev\": \"장치 지정\",\n  \"specifyDevTip\": \"예를 들어, 네트워크 트래픽 통계는 기본적으로 모든 장치를 대상으로 합니다. 여기서 특정 장치를 지정할 수 있습니다.\",\n  \"speed\": \"속도\",\n  \"spentTime\": \"소요 시간: {time}\",\n  \"sshConfigAllExist\": \"모든 서버가 이미 존재합니다 (중복 {duplicateCount}개 발견)\",\n  \"sshConfigDuplicatesSkipped\": \"중복 {duplicateCount}개가 건너뛰어집니다\",\n  \"sshConfigFound\": \"시스템에서 SSH 설정을 발견했습니다.\",\n  \"sshConfigFoundServers\": \"서버 {totalCount}개를 발견했습니다\",\n  \"sshConfigImport\": \"SSH 설정 가져오기\",\n  \"sshConfigImportHelp\": \"기본 정보만 가져올 수 있습니다. 예: IP/포트.\",\n  \"sshConfigImportPermission\": \"~/.ssh/config를 읽고 서버 설정을 자동으로 가져올 수 있는 권한을 부여하시겠습니까?\",\n  \"sshConfigImportTip\": \"첫 서버 생성 시 ~/.ssh/config 읽기 안내\",\n  \"sshConfigImported\": \"SSH 설정에서 서버 {count}개를 가져왔습니다\",\n  \"sshHostKeyChangedDesc\": \"{serverName}의 SSH 호스트 키가 변경되었습니다. 이 서버를 신뢰하는 경우에만 계속 진행하세요.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"지문 (MD5 base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"지문 (MD5 hex): {fingerprint}\",\n  \"sshHostKeyType\": \"SSH 호스트 키 유형\",\n  \"@sshHostKeyType\": {\n    \"description\": \"Label for the SSH host key type displayed in the host key verification dialog.\"\n  },\n  \"sshHostKeyNewDesc\": \"{serverName}에서 새 SSH 호스트 키를 수신했습니다. 신뢰하기 전에 지문을 확인해 주세요.\",\n  \"sshHostKeyStoredFingerprint\": \"저장된 지문: {fingerprint}\",\n  \"sshConfigManualSelect\": \"SSH 설정 파일을 수동으로 선택하시겠습니까?\",\n  \"sshConfigNoServers\": \"SSH 설정에서 서버를 찾을 수 없습니다\",\n  \"sshConfigPermissionDenied\": \"macOS 권한으로 인해 SSH 설정 파일에 접근할 수 없습니다.\",\n  \"sshConfigServersToImport\": \"서버 {importCount}개가 가져오기됩니다\",\n  \"sshTermHelp\": \"터미널이 스크롤 가능할 때, 가로로 드래그하면 텍스트를 선택할 수 있습니다. 키보드 버튼을 누르면 키보드가 켜지거나 꺼집니다. 파일 아이콘은 현재 경로를 SFTP로 엽니다. 클립보드 버튼은 텍스트가 선택되어 있을 때 내용을 복사하고, 텍스트가 선택되어 있지 않고 클립보드에 내용이 있을 때 터미널에 붙여넣습니다. 코드 아이콘은 코드 스니펫을 터미널에 붙여넣고 실행합니다.\",\n  \"sshTip\": \"이 기능은 현재 실험 단계입니다.\\n\\n{url}에서 버그를 보고하거나 개발에 참여해 주세요.\",\n  \"sshVirtualKeyAutoOff\": \"가상 키 자동 전환\",\n  \"stat\": \"통계\",\n  \"supportFmtArgs\": \"다음 형식 매개변수가 지원됩니다:\",\n  \"suspendTip\": \"일시 중지 기능은 root 권한과 systemd 지원이 필요합니다.\",\n  \"switchTo\": \"{val}(으)로 전환\",\n  \"syncTip\": \"일부 변경 사항은 재시작해야 적용될 수 있습니다.\",\n  \"system\": \"시스템\",\n  \"tag\": \"태그\",\n  \"tapToStartDiscovery\": \"검색 버튼을 눌러 네트워크에서 SSH 서버를 검색하세요\",\n  \"termFontSizeTip\": \"이 설정은 터미널 크기(너비 및 높이)에 영향을 줍니다. 현재 세션의 글꼴 크기를 조정하려면 터미널 페이지에서 확대/축소할 수 있습니다.\",\n  \"textScaler\": \"텍스트 배율\",\n  \"textScalerTip\": \"1.0 => 100% (원래 크기), 서버 페이지의 일부 글꼴에만 적용되며 변경을 권장하지 않습니다.\",\n  \"time\": \"시간\",\n  \"times\": \"회\",\n  \"trySudo\": \"sudo 사용 시도\",\n  \"unknown\": \"알 수 없음\",\n  \"unkownConvertMode\": \"알 수 없는 변환 모드\",\n  \"update\": \"업데이트\",\n  \"updateIntervalEqual0\": \"0으로 설정하면 자동으로 업데이트되지 않습니다.\\nCPU 상태를 계산할 수 없습니다.\",\n  \"updateServerStatusInterval\": \"서버 상태 업데이트 간격\",\n  \"upsideDown\": \"상하 반전\",\n  \"useCdn\": \"CDN 사용\",\n  \"useCdnTip\": \"중국 이외 사용자에게는 CDN 사용을 권장합니다. 사용하시겠습니까?\",\n  \"useNoPwd\": \"비밀번호를 사용하지 않습니다\",\n  \"usePodmanByDefault\": \"기본적으로 Podman 사용\",\n  \"used\": \"사용됨\",\n  \"view\": \"보기\",\n  \"viewDetails\": \"상세 보기\",\n  \"viewErr\": \"오류 보기\",\n  \"virtKeyHelpClipboard\": \"터미널에 선택된 텍스트가 있으면 클립보드에 복사하고, 없으면 클립보드 내용을 터미널에 붙여넣습니다.\",\n  \"virtKeyHelpIME\": \"키보드 켜기/끄기\",\n  \"virtKeyHelpSFTP\": \"현재 디렉토리를 SFTP로 열기.\",\n  \"waitConnection\": \"연결이 설정될 때까지 기다려 주세요.\",\n  \"wakeLock\": \"화면 깨우기 유지\",\n  \"watchNotPaired\": \"페어링된 Apple Watch가 없습니다\",\n  \"webdavSettingEmpty\": \"WebDav 설정이 비어 있습니다\",\n  \"whenOpenApp\": \"앱을 열 때\",\n  \"wolTip\": \"WOL (Wake-on-LAN)을 설정하면 서버에 연결할 때마다 WOL 요청이 전송됩니다.\",\n  \"write\": \"쓰기\",\n  \"writeScriptFailTip\": \"스크립트 작성에 실패했습니다. 권한이 부족하거나 디렉토리가 존재하지 않을 수 있습니다.\",\n  \"writeScriptTip\": \"서버 연결 후 시스템 상태를 모니터링하기 위한 스크립트가 `~/.config/server_box` \\n | `/tmp/server_box`에 작성됩니다. 스크립트 내용을 확인할 수 있습니다.\",\n  \"menuGitHubRepository\": \"GitHub 저장소\",\n  \"podmanDockerEmulationDetected\": \"Podman Docker 에뮬레이션이 감지되었습니다. 설정에서 Podman으로 전환해 주세요.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_nl.arb",
    "content": "{\n  \"@@locale\": \"nl\",\n  \"aboutThanks\": \"Met dank aan de volgende mensen die hebben deelgenomen aan.\",\n  \"acceptBeta\": \"Accepteer testversie-updates\",\n  \"addSystemPrivateKeyTip\": \"Er is momenteel geen privésleutel, wilt u degene toevoegen die bij het systeem wordt geleverd (~/.ssh/id_rsa)?\",\n  \"added2List\": \"Toegevoegd aan takenlijst\",\n  \"addr\": \"Adres\",\n  \"alreadyLastDir\": \"Al in de laatst gebruikte map.\",\n  \"askAi\": \"AI vragen\",\n  \"askAiApiKey\": \"API-sleutel\",\n  \"askAiAwaitingResponse\": \"Wachten op AI-reactie...\",\n  \"askAiBaseUrl\": \"Basis-URL\",\n  \"askAiCommandInserted\": \"Commando in terminal ingevoegd\",\n  \"askAiConfigMissing\": \"Configureer {fields} in de instellingen.\",\n  \"askAiConfirmExecute\": \"Bevestigen voor uitvoeren\",\n  \"askAiConversation\": \"AI-gesprek\",\n  \"askAiDisclaimer\": \"AI kan fouten maken. Gebruik het zorgvuldig.\",\n  \"askAiFollowUpHint\": \"Stel een vervolgvraag...\",\n  \"askAiInsertTerminal\": \"In terminal invoegen\",\n  \"askAiNoResponse\": \"Geen reactie\",\n  \"askAiRecommendedCommand\": \"Door AI voorgestelde opdracht\",\n  \"askAiSelectedContent\": \"Geselecteerde inhoud\",\n  \"askAiUsageHint\": \"Gebruikt in de SSH-terminal\",\n  \"atLeastOneTab\": \"Er moet minimaal één tabblad worden geselecteerd\",\n  \"authFailTip\": \"Authenticatie mislukt, controleer of het wachtwoord/sleutel/host/gebruiker, enz., incorrect zijn.\",\n  \"autoBackupConflict\": \"Er kan slechts één automatische back-up tegelijk worden ingeschakeld.\",\n  \"autoConnect\": \"Automatisch verbinden\",\n  \"autoRun\": \"Automatisch uitvoeren\",\n  \"autoUpdateHomeWidget\": \"Automatische update van home-widget\",\n  \"availableTabs\": \"Beschikbare tabbladen\",\n  \"backupEncrypted\": \"Back-up is versleuteld\",\n  \"backupNotEncrypted\": \"Back-up is niet versleuteld\",\n  \"backupPassword\": \"Back-up wachtwoord\",\n  \"backupPasswordRemoved\": \"Back-up wachtwoord verwijderd\",\n  \"backupPasswordSet\": \"Back-up wachtwoord ingesteld\",\n  \"backupPasswordTip\": \"Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.\",\n  \"backupPasswordWrong\": \"Onjuist back-up wachtwoord\",\n  \"backupTip\": \"De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \\nBewaar deze aub veilig.\",\n  \"backupVersionNotMatch\": \"Back-upversie komt niet overeen.\",\n  \"bgRun\": \"Uitvoeren op de achtergrond\",\n  \"bgRunTip\": \"Deze schakelaar betekent alleen dat het programma zal proberen op de achtergrond uit te voeren, of het in de achtergrond kan worden uitgevoerd, hangt af van of de toestemming is ingeschakeld of niet. Voor native Android, schakel \\\"Batterijoptimalisatie\\\" uit in deze app, en voor miui, wijzig de energiebesparingsbeleid naar \\\"Onbeperkt\\\".\",\n  \"clearAllStatsContent\": \"Weet u zeker dat u alle serververbindingsstatistieken wilt wissen? Deze actie kan niet ongedaan worden gemaakt.\",\n  \"clearAllStatsTitle\": \"Alle statistieken wissen\",\n  \"clearServerStatsContent\": \"Weet u zeker dat u de verbindingsstatistieken voor server \\\"{serverName}\\\" wilt wissen? Deze actie kan niet ongedaan worden gemaakt.\",\n  \"clearServerStatsTitle\": \"Statistieken van {serverName} wissen\",\n  \"clearThisServerStats\": \"Statistieken van deze server wissen\",\n  \"compactDatabase\": \"Database comprimeren\",\n  \"compactDatabaseContent\": \"Databasgrootte: {size}\\n\\nDit zal de database opnieuw organiseren om de bestandsgrootte te verkleinen. Geen gegevens worden verwijderd.\",\n  \"closeAfterSave\": \"Opslaan en sluiten\",\n  \"collapseUITip\": \"Of lange lijsten in de UI standaard moeten worden ingeklapt\",\n  \"connectionDetails\": \"Verbindingsdetails\",\n  \"connectionStats\": \"Verbindingsstatistieken\",\n  \"connectionStatsDesc\": \"Bekijk server verbindingssucces ratio en geschiedenis\",\n  \"containerTrySudoTip\": \"Bijvoorbeeld: in de app is de gebruiker ingesteld op aaa, maar Docker is geïnstalleerd onder de rootgebruiker. In dit geval moet u deze optie inschakelen.\",\n  \"containerSudoPasswordRequired\": \"Een sudo-wachtwoord is vereist om toegang te krijgen tot Docker. Voer uw wachtwoord in.\",\n  \"containerSudoPasswordIncorrect\": \"Het sudo-wachtwoord is onjuist of niet toegestaan. Probeer het opnieuw.\",\n  \"convert\": \"Converteren\",\n  \"copyPath\": \"Pad kopiëren\",\n  \"cpuViewAsProgressTip\": \"Toon het gebruik van elke CPU in een voortgangsbalkstijl (oude stijl)\",\n  \"cursorType\": \"Cursortype\",\n  \"customCmd\": \"Aangepaste opdrachten\",\n  \"customCmdHint\": \"\\\"Opdrachtnaam\\\": \\\"Opdracht\\\"\",\n  \"deleteServers\": \"Servers batchgewijs verwijderen\",\n  \"desktopTerminalTip\": \"Opdracht die wordt gebruikt om de terminalemulator te openen bij het starten van SSH-sessies.\",\n  \"dirEmpty\": \"Zorg ervoor dat de map leeg is.\",\n  \"discoverSshServers\": \"SSH-servers ontdekken\",\n  \"discoveryFailed\": \"Ontdekking mislukt\",\n  \"discoverySettings\": \"Ontdekkingsinstellingen\",\n  \"discoverySummary\": \"Ontdekkingssamenvatting\",\n  \"diskHealth\": \"Schijfgezondheid\",\n  \"diskIgnorePath\": \"Pad negeren voor schijf\",\n  \"displayCpuIndex\": \"Toon de CPU-index\",\n  \"dl2Local\": \"Download {fileName} naar lokaal?\",\n  \"dockerEmptyRunningItems\": \"Er zijn geen actieve containers.\\nDit kan komen doordat:\\n- De Docker-installatiegebruiker niet overeenkomt met de gebruikersnaam die is geconfigureerd binnen de app.\\n- De omgevingsvariabele DOCKER_HOST is niet correct gelezen. U kunt deze krijgen door `echo $DOCKER_HOST` in de terminal uit te voeren.\",\n  \"dockerImagesFmt\": \"{count} afbeeldingen\",\n  \"dockerNotInstalled\": \"Docker niet geïnstalleerd\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} actief, {stoppedCount} container gestopt.\",\n  \"dockerStatusRunningFmt\": \"{count} container actief.\",\n  \"doubleColumnMode\": \"Dubbele kolommodus\",\n  \"doubleColumnTip\": \"Deze optie schakelt alleen de functie in, of deze daadwerkelijk kan worden ingeschakeld, hangt af van de breedte van het apparaat\",\n  \"editVirtKeys\": \"Virtuele toetsen bewerken\",\n  \"editorHighlightTip\": \"De huidige codehighlighting-prestaties zijn slechter en kunnen optioneel worden uitgeschakeld om te verbeteren.\",\n  \"enableMdns\": \"mDNS inschakelen\",\n  \"enableMdnsDesc\": \"Gebruik mDNS/Bonjour om SSH-services te ontdekken\",\n  \"envVars\": \"Omgevingsvariabele\",\n  \"experimentalFeature\": \"Experimentele functie\",\n  \"extraArgs\": \"Extra argumenten\",\n  \"fallbackSshDest\": \"Fallback SSH-bestemming\",\n  \"fdroidReleaseTip\": \"Als u deze app van F-Droid heeft gedownload, wordt aanbevolen deze optie uit te schakelen.\",\n  \"fgService\": \"Voorgrondservice\",\n  \"fgServiceTip\": \"Na het inschakelen kunnen sommige apparaatmodellen crashen. Uitschakelen kan ertoe leiden dat sommige modellen SSH-verbindingen niet op de achtergrond kunnen behouden. Sta ServerBox notificatierechten, achtergronduitvoering en zelf-ontwaken toe in systeeminstellingen.\",\n  \"fileTooLarge\": \"Bestand '{file}' te groot {size}, max {sizeMax}\",\n  \"finishedAt\": \"Voltooid om\",\n  \"followSystem\": \"Volg systeem\",\n  \"fontSize\": \"Lettergrootte\",\n  \"fullScreen\": \"Volledig schermmodus\",\n  \"fullScreenJitter\": \"Volledig scherm trilling\",\n  \"fullScreenJitterHelp\": \"Om inbranden van het scherm te voorkomen\",\n  \"fullScreenTip\": \"Moet de volledig schermmodus worden ingeschakeld wanneer het apparaat naar de liggende modus wordt gedraaid? Deze optie is alleen van toepassing op het servertabblad.\",\n  \"goBackQ\": \"Terug gaan?\",\n  \"goto\": \"Ga naar\",\n  \"hideTitleBar\": \"Titelbalk verbergen\",\n  \"highlight\": \"Code-highlight\",\n  \"homeTabs\": \"Home-tabbladen\",\n  \"homeTabsCustomizeDesc\": \"Pas aan welke tabbladen op de startpagina worden weergegeven en hun volgorde\",\n  \"homeWidgetUrlConfig\": \"Home-widget-url configureren\",\n  \"httpFailedWithCode\": \"verzoek mislukt, statuscode: {code}\",\n  \"ignoreCert\": \"Certificaat negeren\",\n  \"image\": \"Afbeelding\",\n  \"imagesList\": \"Lijst met afbeeldingen\",\n  \"installDockerWithUrl\": \"Installeer eerst docker via https://docs.docker.com/engine/install.\",\n  \"invalid\": \"Ongeldig\",\n  \"jumpServer\": \"Spring naar server\",\n  \"keepForeground\": \"Houd de app op de voorgrond!\",\n  \"keepStatusWhenErr\": \"Behoud de laatste serverstatus\",\n  \"keepStatusWhenErrTip\": \"Alleen in geval van een fout tijdens de scriptuitvoering\",\n  \"keyAuth\": \"Sleutelauthenticatie\",\n  \"lastFailure\": \"Laatst gefaald\",\n  \"lastSuccess\": \"Laatst succesvol\",\n  \"letterCache\": \"Lettercaching\",\n  \"letterCacheTip\": \"Aanbevolen om uit te schakelen, maar na het uitschakelen is het niet mogelijk om CJK-tekens in te voeren.\",\n  \"madeWithLove\": \"Gemaakt met ❤️ door {myGithub}\",\n  \"max\": \"max\",\n  \"maxConcurrency\": \"Maximale gelijktijdigheid\",\n  \"maxRetryCount\": \"Aantal serverherverbindingen\",\n  \"maxRetryCountEqual0\": \"Zal opnieuw blijven proberen.\",\n  \"min\": \"min\",\n  \"more\": \"Meer\",\n  \"moveOutServerFuncBtnsHelp\": \"Aan: kan worden weergegeven onder elke kaart op de Server-tabbladpagina. Uit: kan worden weergegeven bovenaan de Serverdetails-pagina.\",\n  \"needHomeDir\": \"Als u een Synology-gebruiker bent, [zie hier](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Gebruikers van andere systemen moeten zoeken hoe ze een home directory kunnen creëren.\",\n  \"needRestart\": \"App moet opnieuw worden gestart\",\n  \"netViewType\": \"Netweergavetype\",\n  \"newContainer\": \"Nieuwe container\",\n  \"noConnectionStatsData\": \"Geen verbindingsstatistiekgegevens\",\n  \"noLineChart\": \"lijndiagrammen gebruiken\",\n  \"noLineChartForCpu\": \"Gebruik geen lijndiagrammen voor CPU\",\n  \"noPrivateKeyTip\": \"De privésleutel bestaat niet, deze is mogelijk verwijderd of er is een configuratiefout.\",\n  \"noPromptAgain\": \"Niet meer vragen\",\n  \"onServerDetailPage\": \"Op serverdetailspagina\",\n  \"onlyOneLine\": \"Alleen als één regel weergeven (scrollbaar)\",\n  \"onlyWhenCoreBiggerThan8\": \"Alleen effectief wanneer het aantal cores > 8\",\n  \"openLastPath\": \"Open het laatste pad\",\n  \"openLastPathTip\": \"Verschillende servers hebben verschillende logs, en de log is het pad naar de uitgang\",\n  \"parseContainerStatsTip\": \"Het parsen van de bezettingsstatus van Docker is relatief langzaam.\",\n  \"percentOfSize\": \"{percent}% van {size}\",\n  \"permission\": \"Machtigingen\",\n  \"pingInputIP\": \"Voer een doel-IP / domein in.\",\n  \"pingNoServer\": \"Geen server om te pingen.\\nVoeg een server toe in het servertabblad.\",\n  \"plugInType\": \"Invoegingstype\",\n  \"preferDiskAmount\": \"Geef de schijfcapaciteit prioriteit bij weergave\",\n  \"privateKey\": \"Privésleutel\",\n  \"privateKeyNotFoundFmt\": \"Privésleutel [{keyId}] niet gevonden.\",\n  \"pushToken\": \"Push-token\",\n  \"pveIgnoreCertTip\": \"Niet aanbevolen om in te schakelen, let op beveiligingsrisico's! Als u de standaardcertificaat van PVE gebruikt, moet u deze optie inschakelen.\",\n  \"pveLoginFailed\": \"Aanmelden mislukt. Kan niet authenticeren met gebruikersnaam/wachtwoord van serverconfiguratie voor Linux PAM-login.\",\n  \"pveVersionLow\": \"Deze functie bevindt zich momenteel in de testfase en is alleen getest op PVE 8+. Gebruik het met voorzichtigheid.\",\n  \"read\": \"Lezen\",\n  \"recentConnections\": \"Recente verbindingen\",\n  \"rememberPwdInMem\": \"Wachtwoord onthouden in geheugen\",\n  \"rememberPwdInMemTip\": \"Gebruikt voor containers, opschorting, enz.\",\n  \"rememberWindowSize\": \"Venstergrootte onthouden\",\n  \"remotePath\": \"Extern pad\",\n  \"result\": \"Resultaat\",\n  \"rotateAngel\": \"Rotatiehoek\",\n  \"sameIdServerExist\": \"Er bestaat al een server met dezelfde ID\",\n  \"save\": \"Opslaan\",\n  \"second\": \"s\",\n  \"serverDetailOrder\": \"Volgorde van widget op detailpagina\",\n  \"serverFuncBtns\": \"Server functieknoppen\",\n  \"serverOrder\": \"Servervolgorde\",\n  \"serverTabRequired\": \"Servertabblad kan niet worden verwijderd\",\n  \"sftpDlPrepare\": \"Voorbereiden om verbinding te maken...\",\n  \"sftpEditorTip\": \"Indien leeg, gebruik de ingebouwde bestandseditor van de app. Indien een waarde aanwezig is, gebruik de editor van de externe server, bijvoorbeeld `vim` (aanbevolen om automatisch te detecteren volgens `EDITOR`).\",\n  \"sftpRmrDirSummary\": \"Gebruik `rm -r` om een map te verwijderen in SFTP.\",\n  \"sftpSSHConnected\": \"SFTP Verbonden\",\n  \"sftpShowFoldersFirst\": \"Mappen eerst weergeven\",\n  \"showDistLogo\": \"Distributielogo weergeven\",\n  \"size\": \"Grootte\",\n  \"softWrap\": \"Zachte wrap\",\n  \"specifyDev\": \"Apparaat specificeren\",\n  \"specifyDevTip\": \"Bijvoorbeeld, netwerkverkeersstatistieken zijn standaard voor alle apparaten. Hier kunt u een specifiek apparaat opgeven.\",\n  \"speed\": \"Snelheid\",\n  \"spentTime\": \"Gebruikte tijd: {time}\",\n  \"sshConfigAllExist\": \"Alle servers bestaan al ({duplicateCount} duplicaten gevonden)\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} duplicaten worden overgeslagen\",\n  \"sshConfigFound\": \"We hebben SSH-configuratie op uw systeem gevonden\",\n  \"sshConfigFoundServers\": \"{totalCount} servers gevonden\",\n  \"sshConfigImport\": \"SSH Configuratie Importeren\",\n  \"sshConfigImportHelp\": \"Alleen basisinformatie kan worden geïmporteerd, bijvoorbeeld: IP/Poort.\",\n  \"sshConfigImportPermission\": \"Wilt u toestemming geven om ~/.ssh/config te lezen en automatisch serverinstellingen te importeren?\",\n  \"sshConfigImportTip\": \"Prompt om ~/.ssh/config te lezen bij het aanmaken van de eerste server\",\n  \"sshConfigImported\": \"{count} servers geïmporteerd uit SSH-configuratie\",\n  \"sshHostKeyChangedDesc\": \"De SSH-hostsleutel voor {serverName} is gewijzigd. Ga alleen verder als u deze server vertrouwt.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Vingerafdruk (MD5 Base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Vingerafdruk (MD5 hex): {fingerprint}\",\n  \"sshHostKeyType\": \"Type SSH-hostsleutel\",\n  \"sshHostKeyNewDesc\": \"Er is een nieuwe SSH-hostsleutel ontvangen van {serverName}. Controleer de vingerafdruk voordat u vertrouwt.\",\n  \"sshHostKeyStoredFingerprint\": \"Opgeslagen vingerafdruk: {fingerprint}\",\n  \"sshConfigManualSelect\": \"Wilt u het SSH-configuratiebestand handmatig selecteren?\",\n  \"sshConfigNoServers\": \"Geen servers gevonden in SSH-configuratie\",\n  \"sshConfigPermissionDenied\": \"Kan geen toegang krijgen tot SSH-configuratiebestand vanwege macOS-rechten.\",\n  \"sshConfigServersToImport\": \"{importCount} servers worden geïmporteerd\",\n  \"sshTermHelp\": \"Wanneer het terminal scrollbaar is, kan horizontaal slepen tekst selecteren. Klikken op de toetsenbordknop schakelt het toetsenbord aan/uit. Het bestandsicoon opent de huidige pad SFTP. De klembordknop kopieert de inhoud wanneer tekst is geselecteerd en plakt inhoud van het klembord in de terminal wanneer geen tekst is geselecteerd en er inhoud op het klembord staat. Het code-icoon plakt codefragmenten in de terminal en voert ze uit.\",\n  \"sshTip\": \"Deze functie bevindt zich momenteel in de experimentele fase.\\n\\nMeld alstublieft bugs op {url} of sluit je aan bij onze ontwikkeling.\",\n  \"sshVirtualKeyAutoOff\": \"Automatisch schakelen van virtuele toetsen\",\n  \"stat\": \"Statistieken\",\n  \"supportFmtArgs\": \"De volgende opmaakparameters worden ondersteund:\",\n  \"suspendTip\": \"De opschortfunctie vereist rootrechten en systemd-ondersteuning.\",\n  \"switchTo\": \"Overschakelen naar {val}\",\n  \"syncTip\": \"Een herstart kan nodig zijn voor sommige wijzigingen om van kracht te worden.\",\n  \"system\": \"Systeem\",\n  \"tag\": \"Labels\",\n  \"tapToStartDiscovery\": \"Tik op de zoekknop om SSH-servers op uw netwerk te ontdekken\",\n  \"termFontSizeTip\": \"Deze instelling heeft invloed op de terminalgrootte (breedte en hoogte). U kunt inzoomen op de terminalpagina om de lettergrootte van de huidige sessie aan te passen.\",\n  \"textScaler\": \"Tekstschaler\",\n  \"textScalerTip\": \"1.0 => 100% (oorspronkelijke grootte), werkt alleen op het gedeelte van de serverpagina van het lettertype, niet aanbevolen om te wijzigen.\",\n  \"time\": \"Tijd\",\n  \"times\": \"Keer\",\n  \"trySudo\": \"Probeer sudo te gebruiken\",\n  \"unknown\": \"Onbekend\",\n  \"unkownConvertMode\": \"Onbekende conversiemodus\",\n  \"update\": \"Bijwerken\",\n  \"updateIntervalEqual0\": \"Het staat op 0, het zal niet automatisch bijwerken\\nCPU status kan niet berekend worden.\",\n  \"updateServerStatusInterval\": \"Interne server status bijwerking interval\",\n  \"upsideDown\": \"Ondersteboven\",\n  \"useCdn\": \"Gebruikt CDN\",\n  \"useCdnTip\": \"Niet-chinese gebruikers worden aangeraden om deze CDN te gebruiken. Wil je dat?\",\n  \"useNoPwd\": \"Er zal geen wachtwoord gebruikt worden\",\n  \"usePodmanByDefault\": \"Valt terug op Podman\",\n  \"used\": \"Gebruikt\",\n  \"view\": \"Weergave\",\n  \"viewDetails\": \"Details bekijken\",\n  \"viewErr\": \"Zie foutmelding\",\n  \"virtKeyHelpClipboard\": \"Kopiëren naar het klembord als de geselecteerde terminal niet leeg is, anders de inhoud van het klembord plakken in de terminal.\",\n  \"virtKeyHelpIME\": \"Toetsenbord aan/uit zetten\",\n  \"virtKeyHelpSFTP\": \"Huidige map openen in SFTP.\",\n  \"waitConnection\": \"Wacht alstublieft tot de verbinding tot stand is gebracht.\",\n  \"wakeLock\": \"Wakker houden\",\n  \"watchNotPaired\": \"Geen gekoppelde Apple Watch\",\n  \"webdavSettingEmpty\": \"Webdav-instelling is leeg\",\n  \"whenOpenApp\": \"Bij het openen van de app\",\n  \"wolTip\": \"Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.\",\n  \"write\": \"Schrijven\",\n  \"writeScriptFailTip\": \"Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.\",\n  \"writeScriptTip\": \"Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \\n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.\",\n  \"podmanDockerEmulationDetected\": \"Podman Docker-emulatie gedetecteerd. Schakel over naar Podman in de instellingen.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_pt.arb",
    "content": "{\n  \"@@locale\": \"pt\",\n  \"aboutThanks\": \"Agradecimentos a todos os participantes.\",\n  \"acceptBeta\": \"Aceitar atualizações da versão de teste\",\n  \"addSystemPrivateKeyTip\": \"Atualmente, não há nenhuma chave privada. Gostaria de adicionar a chave do sistema (~/.ssh/id_rsa)?\",\n  \"added2List\": \"Adicionado à lista de tarefas\",\n  \"addr\": \"Endereço\",\n  \"alreadyLastDir\": \"Já é o diretório mais alto\",\n  \"askAi\": \"Perguntar à IA\",\n  \"askAiApiKey\": \"Chave de API\",\n  \"askAiAwaitingResponse\": \"Aguardando resposta da IA...\",\n  \"askAiBaseUrl\": \"URL base\",\n  \"askAiCommandInserted\": \"Comando inserido no terminal\",\n  \"askAiConfigMissing\": \"Configure {fields} nas configurações.\",\n  \"askAiConfirmExecute\": \"Confirmar antes de executar\",\n  \"askAiConversation\": \"Conversa com a IA\",\n  \"askAiDisclaimer\": \"A IA pode errar. Use com cautela.\",\n  \"askAiFollowUpHint\": \"Faça uma pergunta adicional...\",\n  \"askAiInsertTerminal\": \"Inserir no terminal\",\n  \"askAiNoResponse\": \"Sem resposta\",\n  \"askAiRecommendedCommand\": \"Comando sugerido pela IA\",\n  \"askAiSelectedContent\": \"Conteúdo selecionado\",\n  \"askAiUsageHint\": \"Usado no terminal SSH\",\n  \"atLeastOneTab\": \"Pelo menos uma aba deve ser selecionada\",\n  \"authFailTip\": \"Autenticação falhou, por favor verifique se a senha/chave/host/usuário, etc., estão incorretos.\",\n  \"autoBackupConflict\": \"Apenas um backup automático pode ser ativado por vez\",\n  \"autoConnect\": \"Conexão automática\",\n  \"autoRun\": \"Execução automática\",\n  \"autoUpdateHomeWidget\": \"Atualização automática do widget da tela inicial\",\n  \"availableTabs\": \"Abas disponíveis\",\n  \"backupEncrypted\": \"Backup está criptografado\",\n  \"backupNotEncrypted\": \"Backup não está criptografado\",\n  \"backupPassword\": \"Senha de backup\",\n  \"backupPasswordRemoved\": \"Senha de backup removida\",\n  \"backupPasswordSet\": \"Senha de backup definida\",\n  \"backupPasswordTip\": \"Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.\",\n  \"backupPasswordWrong\": \"Senha de backup incorreta\",\n  \"backupTip\": \"Os dados exportados podem ser criptografados com senha. \\nPor favor, guarde-os com segurança.\",\n  \"backupVersionNotMatch\": \"Versão de backup não compatível, não é possível restaurar\",\n  \"bgRun\": \"Execução em segundo plano\",\n  \"bgRunTip\": \"Este interruptor indica que o programa tentará rodar em segundo plano, mas a capacidade de fazer isso depende das permissões concedidas. No Android nativo, desative a 'Otimização de bateria' para este app, no MIUI, altere a estratégia de economia de energia para 'Sem restrições'.\",\n  \"clearAllStatsContent\": \"Tem certeza de que deseja limpar todas as estatísticas de conexão do servidor? Esta ação não pode ser desfeita.\",\n  \"clearAllStatsTitle\": \"Limpar todas as estatísticas\",\n  \"clearServerStatsContent\": \"Tem certeza de que deseja limpar as estatísticas de conexão para o servidor \\\"{serverName}\\\"? Esta ação não pode ser desfeita.\",\n  \"clearServerStatsTitle\": \"Limpar estatísticas de {serverName}\",\n  \"clearThisServerStats\": \"Limpar estatísticas deste servidor\",\n  \"compactDatabase\": \"Compactar banco de dados\",\n  \"compactDatabaseContent\": \"Tamanho do banco de dados: {size}\\n\\nIsso reorganizará o banco de dados para reduzir o tamanho do arquivo. Nenhum dado será excluído.\",\n  \"closeAfterSave\": \"Salvar e fechar\",\n  \"collapseUITip\": \"Deve colapsar listas longas na UI por padrão?\",\n  \"connectionDetails\": \"Detalhes da conexão\",\n  \"connectionStats\": \"Estatísticas de conexão\",\n  \"connectionStatsDesc\": \"Ver taxa de sucesso de conexão do servidor e histórico\",\n  \"containerTrySudoTip\": \"Por exemplo: se o usuário for definido como aaa dentro do app, mas o Docker estiver instalado sob o usuário root, esta opção precisará ser ativada\",\n  \"containerSudoPasswordRequired\": \"É necessária uma senha sudo para acessar o Docker. Por favor, insira sua senha.\",\n  \"containerSudoPasswordIncorrect\": \"A senha sudo está incorreta ou não é permitida. Por favor, tente novamente.\",\n  \"convert\": \"Converter\",\n  \"copyPath\": \"Copiar caminho\",\n  \"cpuViewAsProgressTip\": \"Exiba a taxa de uso de cada CPU em estilo de barra de progresso (estilo antigo)\",\n  \"cursorType\": \"Tipo de cursor\",\n  \"customCmd\": \"Comandos personalizados\",\n  \"customCmdHint\": \"\\\"Nome do comando\\\": \\\"Comando\\\"\",\n  \"deleteServers\": \"Excluir servidores em lote\",\n  \"desktopTerminalTip\": \"Comando usado para abrir o emulador de terminal ao iniciar sessões SSH.\",\n  \"dirEmpty\": \"Certifique-se de que a pasta está vazia\",\n  \"discoverSshServers\": \"Descobrir servidores SSH\",\n  \"discoveryFailed\": \"Descoberta falhou\",\n  \"discoverySettings\": \"Configurações de descoberta\",\n  \"discoverySummary\": \"Resumo da descoberta\",\n  \"diskHealth\": \"Saúde do disco\",\n  \"diskIgnorePath\": \"Caminhos de disco ignorados\",\n  \"displayCpuIndex\": \"Exiba o índice de CPU\",\n  \"dl2Local\": \"Baixar {fileName} para o local?\",\n  \"dockerEmptyRunningItems\": \"Não há contêineres em execução.\\nIsso pode ser porque:\\n- O usuário que instalou o Docker difere do usuário configurado no app\\n- A variável de ambiente DOCKER_HOST não foi lida corretamente. Você pode verificar isso executando `echo $DOCKER_HOST` no terminal.\",\n  \"dockerImagesFmt\": \"Total de {count} imagens\",\n  \"dockerNotInstalled\": \"Docker não instalado\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} em execução, {stoppedCount} parados\",\n  \"dockerStatusRunningFmt\": \"{count} contêiner(es) em execução\",\n  \"doubleColumnMode\": \"Modo de coluna dupla\",\n  \"doubleColumnTip\": \"Esta opção apenas ativa a funcionalidade, se ela será ativada depende também da largura do dispositivo\",\n  \"editVirtKeys\": \"Editar teclas virtuais\",\n  \"editorHighlightTip\": \"O desempenho do destaque de código atualmente é ruim, pode optar por desativá-lo para melhorar.\",\n  \"enableMdns\": \"Ativar mDNS\",\n  \"enableMdnsDesc\": \"Usar mDNS/Bonjour para descobrir serviços SSH\",\n  \"envVars\": \"Variável de ambiente\",\n  \"experimentalFeature\": \"Recurso experimental\",\n  \"extraArgs\": \"Argumentos extras\",\n  \"fallbackSshDest\": \"Destino SSH de fallback\",\n  \"fdroidReleaseTip\": \"Se você baixou este aplicativo do F-Droid, é recomendado desativar esta opção.\",\n  \"fgService\": \"Serviço em primeiro plano\",\n  \"fgServiceTip\": \"Após ativar, alguns modelos de dispositivos podem travar. Desativar pode fazer com que alguns modelos não consigam manter conexões SSH em segundo plano. Por favor, permita as permissões de notificação do ServerBox, execução em segundo plano e auto-despertar nas configurações do sistema.\",\n  \"fileTooLarge\": \"Arquivo '{file}' muito grande '{size}', excedendo {sizeMax}\",\n  \"finishedAt\": \"Terminado em\",\n  \"followSystem\": \"Seguir sistema\",\n  \"fontSize\": \"Tamanho da fonte\",\n  \"fullScreen\": \"Modo tela cheia\",\n  \"fullScreenJitter\": \"Tremulação em tela cheia\",\n  \"fullScreenJitterHelp\": \"Prevenir burn-in de tela\",\n  \"fullScreenTip\": \"Deve ser ativado o modo de tela cheia quando o dispositivo é girado para o modo paisagem? Esta opção aplica-se apenas à aba do servidor.\",\n  \"goBackQ\": \"Voltar?\",\n  \"goto\": \"Ir para\",\n  \"hideTitleBar\": \"Ocultar barra de título\",\n  \"highlight\": \"Destaque de código\",\n  \"homeTabs\": \"Abas iniciais\",\n  \"homeTabsCustomizeDesc\": \"Personalize quais abas aparecem na página inicial e sua ordem\",\n  \"homeWidgetUrlConfig\": \"Configuração de URL do widget da tela inicial\",\n  \"httpFailedWithCode\": \"Falha na solicitação, código de status: {code}\",\n  \"ignoreCert\": \"Ignorar certificado\",\n  \"image\": \"Imagem\",\n  \"imagesList\": \"Lista de imagens\",\n  \"installDockerWithUrl\": \"Por favor, instale o Docker primeiro em https://docs.docker.com/engine/install\",\n  \"invalid\": \"Inválido\",\n  \"jumpServer\": \"Servidor de salto\",\n  \"keepForeground\": \"Por favor, mantenha o app em primeiro plano!\",\n  \"keepStatusWhenErr\": \"Manter o status anterior do servidor\",\n  \"keepStatusWhenErrTip\": \"Limitado a erros de execução de scripts\",\n  \"keyAuth\": \"Autenticação por chave\",\n  \"lastFailure\": \"Última falha\",\n  \"lastSuccess\": \"Último sucesso\",\n  \"letterCache\": \"Cache de letras\",\n  \"letterCacheTip\": \"Recomendado desativar, mas após desativar, será impossível inserir caracteres CJK.\",\n  \"madeWithLove\": \"Feito com ❤️ por {myGithub}\",\n  \"max\": \"Máximo\",\n  \"maxConcurrency\": \"Concorrência máxima\",\n  \"maxRetryCount\": \"Número de tentativas de reconexão com o servidor\",\n  \"maxRetryCountEqual0\": \"Irá tentar indefinidamente\",\n  \"min\": \"Mínimo\",\n  \"more\": \"Mais\",\n  \"moveOutServerFuncBtnsHelp\": \"Ativado: Mostra abaixo de cada cartão na aba do servidor. Desativado: Mostra no topo da página de detalhes do servidor.\",\n  \"needHomeDir\": \"Se você é usuário de Synology, [veja aqui](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Usuários de outros sistemas precisam pesquisar como criar um diretório home.\",\n  \"needRestart\": \"Necessita reiniciar o app\",\n  \"netViewType\": \"Tipo de visualização de rede\",\n  \"newContainer\": \"Novo contêiner\",\n  \"noConnectionStatsData\": \"Não há dados de estatísticas de conexão\",\n  \"noLineChart\": \"Não usar gráficos de linha\",\n  \"noLineChartForCpu\": \"Não utilizar gráficos de linhas para a CPU\",\n  \"noPrivateKeyTip\": \"A chave privada não existe, pode ter sido deletada ou há um erro de configuração.\",\n  \"noPromptAgain\": \"Não perguntar novamente\",\n  \"onServerDetailPage\": \"Na página de detalhes do servidor\",\n  \"onlyOneLine\": \"Exibir apenas como uma linha (rolável)\",\n  \"onlyWhenCoreBiggerThan8\": \"Efetivo apenas quando o número de núcleos > 8\",\n  \"openLastPath\": \"Abrir o último caminho\",\n  \"openLastPathTip\": \"Registros diferentes para servidores diferentes, e registra o caminho ao sair\",\n  \"parseContainerStatsTip\": \"Análise de status do Docker pode ser lenta\",\n  \"percentOfSize\": \"{percent}% de {size}\",\n  \"permission\": \"Permissões\",\n  \"pingInputIP\": \"Por favor, insira o IP ou domínio alvo\",\n  \"pingNoServer\": \"Nenhum servidor disponível para Ping\\nPor favor, adicione um servidor na aba de servidores e tente novamente\",\n  \"plugInType\": \"Tipo de Inserção\",\n  \"preferDiskAmount\": \"Priorizar a exibição da capacidade do disco\",\n  \"privateKey\": \"Chave privada\",\n  \"privateKeyNotFoundFmt\": \"Chave privada [{keyId}] não encontrada.\",\n  \"pushToken\": \"Token de notificação push\",\n  \"pveIgnoreCertTip\": \"Não recomendado para ativar, cuidado com os riscos de segurança! Se estiver usando o certificado padrão do PVE, você precisa habilitar esta opção.\",\n  \"pveLoginFailed\": \"Falha no login. Não é possível autenticar com o nome de usuário/senha da configuração do servidor para login no Linux PAM.\",\n  \"pveVersionLow\": \"Esta funcionalidade está atualmente em fase de teste e foi testada apenas no PVE 8+. Por favor, use com cautela.\",\n  \"read\": \"Leitura\",\n  \"recentConnections\": \"Conexões recentes\",\n  \"rememberPwdInMem\": \"Lembrar senha na memória\",\n  \"rememberPwdInMemTip\": \"Usado para contêineres, suspensão, etc.\",\n  \"rememberWindowSize\": \"Lembrar o tamanho da janela\",\n  \"remotePath\": \"Caminho remoto\",\n  \"result\": \"Resultado\",\n  \"rotateAngel\": \"Ângulo de rotação\",\n  \"sameIdServerExist\": \"Já existe um servidor com o mesmo ID\",\n  \"save\": \"Salvar\",\n  \"second\": \"Segundo\",\n  \"serverDetailOrder\": \"Ordem dos componentes na página de detalhes do servidor\",\n  \"serverFuncBtns\": \"Botões de função do servidor\",\n  \"serverOrder\": \"Ordem do servidor\",\n  \"serverTabRequired\": \"A aba do servidor não pode ser removida\",\n  \"sftpDlPrepare\": \"Preparando para conectar ao servidor...\",\n  \"sftpEditorTip\": \"Se vazio, use o editor de arquivos integrado do aplicativo. Se houver um valor, use o editor do servidor remoto, por exemplo, `vim` (recomendado detectar automaticamente de acordo com `EDITOR`).\",\n  \"sftpRmrDirSummary\": \"Usar `rm -r` em SFTP para excluir pastas\",\n  \"sftpSSHConnected\": \"SFTP conectado...\",\n  \"sftpShowFoldersFirst\": \"Mostrar pastas primeiro\",\n  \"showDistLogo\": \"Mostrar logo da distribuição\",\n  \"size\": \"Tamanho\",\n  \"softWrap\": \"Quebra de linha suave\",\n  \"specifyDev\": \"Especificar dispositivo\",\n  \"specifyDevTip\": \"Por exemplo, as estatísticas de tráfego de rede são por padrão para todos os dispositivos. Você pode especificar um dispositivo específico aqui.\",\n  \"speed\": \"Velocidade\",\n  \"spentTime\": \"Tempo gasto: {time}\",\n  \"sshConfigAllExist\": \"Todos os servidores já existem (encontradas {duplicateCount} duplicatas)\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} duplicatas serão ignoradas\",\n  \"sshConfigFound\": \"Encontramos configuração SSH no seu sistema\",\n  \"sshConfigFoundServers\": \"Encontrados {totalCount} servidores\",\n  \"sshConfigImport\": \"Importar Configuração SSH\",\n  \"sshConfigImportHelp\": \"Só é possível importar informações básicas, por exemplo: IP/Porta.\",\n  \"sshConfigImportPermission\": \"Gostaria de dar permissão para ler ~/.ssh/config e importar automaticamente as configurações do servidor?\",\n  \"sshConfigImportTip\": \"Sugestão para ler ~/.ssh/config na criação do primeiro servidor\",\n  \"sshConfigImported\": \"Importados {count} servidores da configuração SSH\",\n  \"sshHostKeyChangedDesc\": \"A chave de host SSH de {serverName} foi alterada. Continue apenas se confiar neste servidor.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Impressão digital (MD5 Base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Impressão digital (MD5 hex): {fingerprint}\",\n  \"sshHostKeyType\": \"Tipo de chave de host SSH\",\n  \"sshHostKeyNewDesc\": \"Uma nova chave de host SSH foi recebida de {serverName}. Verifique a impressão digital antes de confiar.\",\n  \"sshHostKeyStoredFingerprint\": \"Impressão digital armazenada: {fingerprint}\",\n  \"sshConfigManualSelect\": \"Gostaria de selecionar manualmente o arquivo de configuração SSH?\",\n  \"sshConfigNoServers\": \"Nenhum servidor encontrado na configuração SSH\",\n  \"sshConfigPermissionDenied\": \"Não é possível acessar o arquivo de configuração SSH devido às permissões do macOS.\",\n  \"sshConfigServersToImport\": \"{importCount} servidores serão importados\",\n  \"sshTermHelp\": \"Quando o terminal é rolável, arrastar horizontalmente pode selecionar texto. Clicar no botão do teclado ativa/desativa o teclado. O ícone de arquivo abre o SFTP do caminho atual. O botão da área de transferência copia o conteúdo quando o texto é selecionado e cola o conteúdo da área de transferência no terminal quando nenhum texto é selecionado e há conteúdo na área de transferência. O ícone de código cola trechos de código no terminal e os executa.\",\n  \"sshTip\": \"Esta funcionalidade está em fase de teste.\\n\\nPor favor, reporte problemas em {url} ou junte-se a nós no desenvolvimento.\",\n  \"sshVirtualKeyAutoOff\": \"Desativação automática das teclas virtuais\",\n  \"stat\": \"Estatísticas\",\n  \"supportFmtArgs\": \"Suporta os seguintes argumentos formatados:\",\n  \"suspendTip\": \"A função de suspensão requer permissões de root e suporte do systemd.\",\n  \"switchTo\": \"Mudar para {val}\",\n  \"syncTip\": \"Pode ser necessário reiniciar para algumas mudanças surtirem efeito.\",\n  \"system\": \"Sistema\",\n  \"tag\": \"Tag\",\n  \"tapToStartDiscovery\": \"Toque no botão de pesquisa para descobrir servidores SSH na sua rede\",\n  \"termFontSizeTip\": \"Esta configuração afetará o tamanho do terminal (largura e altura). Você pode dar zoom na página do terminal para ajustar o tamanho da fonte da sessão atual.\",\n  \"textScaler\": \"Escala de texto\",\n  \"textScalerTip\": \"1.0 => 100% (tamanho original), afeta apenas algumas fontes na página do servidor, não é recomendado alterar.\",\n  \"time\": \"Tempo\",\n  \"times\": \"Vezes\",\n  \"trySudo\": \"Tentar usar sudo\",\n  \"unknown\": \"Desconhecido\",\n  \"unkownConvertMode\": \"Modo de conversão desconhecido\",\n  \"update\": \"Atualizar\",\n  \"updateIntervalEqual0\": \"Se definido como 0, o estado do servidor não será atualizado automaticamente.\\nE o uso da CPU não poderá ser calculado.\",\n  \"updateServerStatusInterval\": \"Intervalo de atualização do estado do servidor\",\n  \"upsideDown\": \"Inverter verticalmente\",\n  \"useCdn\": \"Utilizando CDN\",\n  \"useCdnTip\": \"Recomenda-se que usuários não chineses usem CDN. Gostaria de usá-lo?\",\n  \"useNoPwd\": \"Será usado sem senha\",\n  \"usePodmanByDefault\": \"Usar Podman por padrão\",\n  \"used\": \"Usado\",\n  \"view\": \"Visualização\",\n  \"viewDetails\": \"Ver detalhes\",\n  \"viewErr\": \"Ver erro\",\n  \"virtKeyHelpClipboard\": \"Se houver texto selecionado no terminal, copia para a área de transferência, caso contrário, cola o conteúdo da área de transferência no terminal.\",\n  \"virtKeyHelpIME\": \"Ligar/desligar o teclado\",\n  \"virtKeyHelpSFTP\": \"Abre o caminho atual em SFTP.\",\n  \"waitConnection\": \"Por favor, aguarde a conexão ser estabelecida\",\n  \"wakeLock\": \"Manter acordado\",\n  \"watchNotPaired\": \"Não há Apple Watch pareado\",\n  \"webdavSettingEmpty\": \"Configurações de Webdav estão vazias\",\n  \"whenOpenApp\": \"Ao abrir o app\",\n  \"wolTip\": \"Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.\",\n  \"write\": \"Escrita\",\n  \"writeScriptFailTip\": \"Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.\",\n  \"writeScriptTip\": \"Após conectar ao servidor, um script será escrito em `~/.config/server_box` \\n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.\",\n  \"podmanDockerEmulationDetected\": \"Emulação Podman Docker detectada. Por favor, alterne para Podman nas configurações.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_ru.arb",
    "content": "{\n  \"@@locale\": \"ru\",\n  \"aboutThanks\": \"Благодарности всем участникам.\",\n  \"acceptBeta\": \"Принять обновления тестовой версии\",\n  \"addSystemPrivateKeyTip\": \"В данный момент приватные ключи отсутствуют. Добавить системный приватный ключ (~/.ssh/id_rsa)?\",\n  \"added2List\": \"Добавлено в список задач\",\n  \"addr\": \"Адрес\",\n  \"alreadyLastDir\": \"Уже в корневом каталоге\",\n  \"askAi\": \"Спросить ИИ\",\n  \"askAiApiKey\": \"Ключ API\",\n  \"askAiAwaitingResponse\": \"Ожидание ответа ИИ...\",\n  \"askAiBaseUrl\": \"Базовый URL\",\n  \"askAiCommandInserted\": \"Команда вставлена в терминал\",\n  \"askAiConfigMissing\": \"Настройте {fields} в настройках.\",\n  \"askAiConfirmExecute\": \"Подтвердите перед выполнением\",\n  \"askAiConversation\": \"Разговор с ИИ\",\n  \"askAiDisclaimer\": \"ИИ может ошибаться. Используйте с осторожностью.\",\n  \"askAiFollowUpHint\": \"Задайте дополнительный вопрос...\",\n  \"askAiInsertTerminal\": \"Вставить в терминал\",\n  \"askAiNoResponse\": \"Нет ответа\",\n  \"askAiRecommendedCommand\": \"Команда, предложенная ИИ\",\n  \"askAiSelectedContent\": \"Выбранное содержимое\",\n  \"askAiUsageHint\": \"Используется в SSH-терминале\",\n  \"atLeastOneTab\": \"Должна быть выбрана хотя бы одна вкладка\",\n  \"authFailTip\": \"Аутентификация не удалась, пожалуйста, проверьте, правильны ли пароль/ключ/хост/пользователь и т.д.\",\n  \"autoBackupConflict\": \"Может быть включено только одно автоматическое резервное копирование\",\n  \"autoConnect\": \"Автоматическое подключение\",\n  \"autoRun\": \"Автозапуск\",\n  \"autoUpdateHomeWidget\": \"Автоматическое обновление виджета на главном экране\",\n  \"availableTabs\": \"Доступные вкладки\",\n  \"backupEncrypted\": \"Резервная копия зашифрована\",\n  \"backupNotEncrypted\": \"Резервная копия не зашифрована\",\n  \"backupPassword\": \"Пароль резервной копии\",\n  \"backupPasswordRemoved\": \"Пароль резервной копии удален\",\n  \"backupPasswordSet\": \"Пароль резервной копии установлен\",\n  \"backupPasswordTip\": \"Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.\",\n  \"backupPasswordWrong\": \"Неверный пароль резервной копии\",\n  \"backupTip\": \"Экспортированные данные могут быть зашифрованы паролем. \\nПожалуйста, храните их в безопасности.\",\n  \"backupVersionNotMatch\": \"Версия резервной копии не совпадает, восстановление невозможно\",\n  \"bgRun\": \"Работа в фоновом режиме\",\n  \"bgRunTip\": \"Этот переключатель означает, что программа будет пытаться работать в фоновом режиме, но фактическое выполнение зависит от того, включено ли разрешение. Для нативного Android отключите «Оптимизацию батареи» для этого приложения, для MIUI измените контроль активности на «Нет ограничений».\",\n  \"clearAllStatsContent\": \"Вы уверены, что хотите очистить всю статистику соединений сервера? Это действие не может быть отменено.\",\n  \"clearAllStatsTitle\": \"Очистить всю статистику\",\n  \"clearServerStatsContent\": \"Вы уверены, что хотите очистить статистику соединений для сервера \\\"{serverName}\\\"? Это действие не может быть отменено.\",\n  \"clearServerStatsTitle\": \"Очистить статистику {serverName}\",\n  \"clearThisServerStats\": \"Очистить статистику этого сервера\",\n  \"compactDatabase\": \"Сжать базу данных\",\n  \"compactDatabaseContent\": \"Размер базы данных: {size}\\n\\nЭто перестроит базу данных для уменьшения размера файла. Данные не будут удалены.\",\n  \"closeAfterSave\": \"Сохранить и закрыть\",\n  \"collapseUITip\": \"Свернуть длинные списки в UI по умолчанию\",\n  \"connectionDetails\": \"Детали соединения\",\n  \"connectionStats\": \"Статистика соединений\",\n  \"connectionStatsDesc\": \"Просмотр коэффициента успешности подключения к серверу и истории\",\n  \"containerTrySudoTip\": \"Например: если пользователь в приложении установлен как aaa, но Docker установлен под пользователем root, тогда нужно включить эту опцию\",\n  \"containerSudoPasswordRequired\": \"Для доступа к Docker требуется пароль sudo. Пожалуйста, введите ваш пароль.\",\n  \"containerSudoPasswordIncorrect\": \"Пароль sudo неверен или не разрешён. Пожалуйста, попробуйте снова.\",\n  \"convert\": \"Конвертировать\",\n  \"copyPath\": \"Копировать путь\",\n  \"cpuViewAsProgressTip\": \"Отобразите уровень использования каждого процессора в виде индикатора выполнения (старый стиль)\",\n  \"cursorType\": \"Тип курсора\",\n  \"customCmd\": \"Пользовательские команды\",\n  \"customCmdHint\": \"\\\"Имя команды\\\": \\\"Команда\\\"\",\n  \"deleteServers\": \"Удалить серверы пакетно\",\n  \"desktopTerminalTip\": \"Команда для открытия эмулятора терминала при запуске SSH-сеансов.\",\n  \"dirEmpty\": \"Пожалуйста, убедитесь, что папка пуста\",\n  \"discoverSshServers\": \"Обнаружить SSH серверы\",\n  \"discoveryFailed\": \"Обнаружение не удалось\",\n  \"discoverySettings\": \"Настройки обнаружения\",\n  \"discoverySummary\": \"Сводка обнаружения\",\n  \"diskHealth\": \"Состояние диска\",\n  \"diskIgnorePath\": \"Игнорировать путь к диску\",\n  \"displayCpuIndex\": \"Отобразить индекс ЦП\",\n  \"dl2Local\": \"Загрузить {fileName} на локальный диск?\",\n  \"dockerEmptyRunningItems\": \"Нет запущенных контейнеров.\\nЭто может быть из-за:\\n- пользователя Docker, отличного от пользователя, настроенного в приложении\\n- переменной окружения DOCKER_HOST, которая не была правильно считана. Вы можете выполнить `echo $DOCKER_HOST` в терминале, чтобы увидеть ее значение.\",\n  \"dockerImagesFmt\": \"Всего {count} образов\",\n  \"dockerNotInstalled\": \"Docker не установлен\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} запущено, {stoppedCount} остановлено\",\n  \"dockerStatusRunningFmt\": \"{count} контейнеров запущено\",\n  \"doubleColumnMode\": \"Режим двойной колонки\",\n  \"doubleColumnTip\": \"Эта опция лишь включает функцию; фактическое применение зависит от ширины устройства\",\n  \"editVirtKeys\": \"Редактировать виртуальные клавиши\",\n  \"editorHighlightTip\": \"Текущая производительность подсветки кода неудовлетворительна, можно отключить для улучшения.\",\n  \"enableMdns\": \"Включить mDNS\",\n  \"enableMdnsDesc\": \"Использовать mDNS/Bonjour для обнаружения SSH служб\",\n  \"envVars\": \"Переменная окружения\",\n  \"experimentalFeature\": \"Экспериментальная функция\",\n  \"extraArgs\": \"Дополнительные аргументы\",\n  \"fallbackSshDest\": \"Резервное место назначения SSH\",\n  \"fdroidReleaseTip\": \"Если вы скачали это приложение с F-Droid, рекомендуется отключить эту опцию.\",\n  \"fgService\": \"Сервис переднего плана\",\n  \"fgServiceTip\": \"После включения некоторые модели устройств могут вылетать. Отключение может привести к тому, что некоторые модели не смогут поддерживать SSH-соединения в фоновом режиме. Пожалуйста, разрешите ServerBox права на уведомления, фоновую работу и самопробуждение в системных настройках.\",\n  \"fileTooLarge\": \"Файл '{file}' слишком большой '{size}', превышает {sizeMax}\",\n  \"finishedAt\": \"Завершено в\",\n  \"followSystem\": \"Следовать за системой\",\n  \"fontSize\": \"Размер шрифта\",\n  \"fullScreen\": \"Полноэкранный режим\",\n  \"fullScreenJitter\": \"Вибрация в полноэкранном режиме\",\n  \"fullScreenJitterHelp\": \"Предотвращение выгорания экрана\",\n  \"fullScreenTip\": \"Следует ли включить полноэкранный режим, когда устройство поворачивается в альбомный режим? Эта опция применяется только к вкладке сервера.\",\n  \"goBackQ\": \"Вернуться?\",\n  \"goto\": \"Перейти к\",\n  \"hideTitleBar\": \"Скрыть заголовок\",\n  \"highlight\": \"Подсветка кода\",\n  \"homeTabs\": \"Вкладки дома\",\n  \"homeTabsCustomizeDesc\": \"Настройте, какие вкладки появляются на главной странице и их порядок\",\n  \"homeWidgetUrlConfig\": \"Конфигурация URL виджета домашнего экрана\",\n  \"httpFailedWithCode\": \"ошибка запроса, код: {code}\",\n  \"ignoreCert\": \"Игнорировать сертификат\",\n  \"image\": \"Образ\",\n  \"imagesList\": \"Список образов\",\n  \"installDockerWithUrl\": \"Пожалуйста, сначала установите Docker по адресу https://docs.docker.com/engine/install\",\n  \"invalid\": \"Недействительный\",\n  \"jumpServer\": \"прыжковый сервер\",\n  \"keepForeground\": \"Пожалуйста, держите приложение в фокусе!\",\n  \"keepStatusWhenErr\": \"Сохранять статус сервера при ошибке\",\n  \"keepStatusWhenErrTip\": \"Применимо только в случае ошибки выполнения скрипта\",\n  \"keyAuth\": \"Аутентификация по ключу\",\n  \"lastFailure\": \"Последний сбой\",\n  \"lastSuccess\": \"Последний успех\",\n  \"letterCache\": \"Кэширование букв\",\n  \"letterCacheTip\": \"Рекомендуется отключить, но после отключения будет невозможно вводить символы CJK.\",\n  \"madeWithLove\": \"Создано с ❤️ by {myGithub}\",\n  \"max\": \"максимум\",\n  \"maxConcurrency\": \"Максимальная параллельность\",\n  \"maxRetryCount\": \"Максимальное количество попыток переподключения к серверу\",\n  \"maxRetryCountEqual0\": \"Будет бесконечно пытаться переподключиться\",\n  \"min\": \"минимум\",\n  \"more\": \"Больше\",\n  \"moveOutServerFuncBtnsHelp\": \"Включено: кнопки функций сервера отображаются под каждой карточкой на вкладке сервера. Выключено: отображается в верхней части страницы деталей сервера.\",\n  \"needHomeDir\": \"Если вы пользователь Synology, [смотрите здесь](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Пользователям других систем нужно искать, как создать домашний каталог.\",\n  \"needRestart\": \"Требуется перезапуск приложения\",\n  \"netViewType\": \"Тип визуализации сети\",\n  \"newContainer\": \"Создать контейнер\",\n  \"noConnectionStatsData\": \"Нет данных статистики соединений\",\n  \"noLineChart\": \"Не использовать линейные графики\",\n  \"noLineChartForCpu\": \"Не используйте линейные графики для ЦП\",\n  \"noPrivateKeyTip\": \"Приватный ключ не существует, возможно, он был удален или есть ошибка в настройках.\",\n  \"noPromptAgain\": \"Больше не спрашивать\",\n  \"onServerDetailPage\": \"На странице деталей сервера\",\n  \"onlyOneLine\": \"Отображать только в одной строке (прокручивается)\",\n  \"onlyWhenCoreBiggerThan8\": \"Действует только при количестве ядер больше 8\",\n  \"openLastPath\": \"Открыть последний путь\",\n  \"openLastPathTip\": \"Для разных серверов будут сохранены разные записи, записывается путь при выходе\",\n  \"parseContainerStatsTip\": \"Анализ статуса использования Docker может быть медленным\",\n  \"percentOfSize\": \"{percent}% от {size}\",\n  \"permission\": \"Разрешения\",\n  \"pingInputIP\": \"Пожалуйста, введите целевой IP или домен\",\n  \"pingNoServer\": \"Нет доступных серверов для Ping\\nПожалуйста, добавьте их на вкладке «Сервер» и попробуйте снова\",\n  \"plugInType\": \"Тип вставки\",\n  \"preferDiskAmount\": \"Приоритетное отображение объёма диска\",\n  \"privateKey\": \"Приватный ключ\",\n  \"privateKeyNotFoundFmt\": \"Закрытый ключ [{keyId}] не найден.\",\n  \"pushToken\": \"Токен уведомлений\",\n  \"pveIgnoreCertTip\": \"Не рекомендуется включать, обратите внимание на риски безопасности! Если вы используете стандартный сертификат от PVE, вам нужно включить эту опцию.\",\n  \"pveLoginFailed\": \"Ошибка входа. Невозможно аутентифицироваться с помощью имени пользователя/пароля из конфигурации сервера для входа в Linux PAM.\",\n  \"pveVersionLow\": \"Эта функция в настоящее время находится на стадии тестирования и была протестирована только на PVE 8+. Используйте ее с осторожностью.\",\n  \"read\": \"Чтение\",\n  \"recentConnections\": \"Недавние соединения\",\n  \"rememberPwdInMem\": \"Запомнить пароль в памяти\",\n  \"rememberPwdInMemTip\": \"Используется для контейнеров, приостановки и т. д.\",\n  \"rememberWindowSize\": \"Запомнить размер окна\",\n  \"remotePath\": \"Удаленный путь\",\n  \"result\": \"Результат\",\n  \"rotateAngel\": \"Угол поворота\",\n  \"sameIdServerExist\": \"Сервер с таким ID уже существует\",\n  \"save\": \"Сохранить\",\n  \"second\": \"с\",\n  \"serverDetailOrder\": \"Порядок элементов на странице деталей сервера\",\n  \"serverFuncBtns\": \"Кнопки функций сервера\",\n  \"serverOrder\": \"Порядок серверов\",\n  \"serverTabRequired\": \"Вкладку сервера нельзя удалить\",\n  \"sftpDlPrepare\": \"Подготовка подключения...\",\n  \"sftpEditorTip\": \"Если пусто, используйте встроенный редактор файлов приложения. Если значение указано, используйте редактор удаленного сервера, например, `vim` (рекомендуется автоматически определять согласно `EDITOR`).\",\n  \"sftpRmrDirSummary\": \"Использовать `rm -r` в SFTP для удаления папок\",\n  \"sftpSSHConnected\": \"SFTP подключен...\",\n  \"sftpShowFoldersFirst\": \"Показывать папки в начале\",\n  \"showDistLogo\": \"Показать лого дистрибутива\",\n  \"size\": \"Размер\",\n  \"softWrap\": \"Мягкий перенос\",\n  \"specifyDev\": \"Указать устройство\",\n  \"specifyDevTip\": \"Например, статистика сетевого трафика по умолчанию относится ко всем устройствам. Здесь вы можете указать конкретное устройство.\",\n  \"speed\": \"Скорость\",\n  \"spentTime\": \"Затрачено времени: {time}\",\n  \"sshConfigAllExist\": \"Все серверы уже существуют (найдено {duplicateCount} дубликатов)\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} дубликатов будут пропущены\",\n  \"sshConfigFound\": \"Мы нашли SSH-конфигурацию в вашей системе\",\n  \"sshConfigFoundServers\": \"Найдено {totalCount} серверов\",\n  \"sshConfigImport\": \"Импорт SSH Конфигурации\",\n  \"sshConfigImportHelp\": \"Можно импортировать только базовую информацию, например: IP/порт.\",\n  \"sshConfigImportPermission\": \"Хотите ли вы дать разрешение на чтение ~/.ssh/config и автоматический импорт настроек сервера?\",\n  \"sshConfigImportTip\": \"Предложение прочитать ~/.ssh/config при создании первого сервера\",\n  \"sshConfigImported\": \"Импортировано {count} серверов из SSH-конфигурации\",\n  \"sshHostKeyChangedDesc\": \"SSH-ключ хоста для {serverName} изменился. Продолжайте только если доверяете этому серверу.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Отпечаток (MD5 Base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Отпечаток (MD5 hex): {fingerprint}\",\n  \"sshHostKeyType\": \"Тип ключа хоста SSH\",\n  \"sshHostKeyNewDesc\": \"Получен новый SSH-ключ хоста от {serverName}. Проверьте отпечаток перед продолжением.\",\n  \"sshHostKeyStoredFingerprint\": \"Сохранённый отпечаток: {fingerprint}\",\n  \"sshConfigManualSelect\": \"Хотели бы вы вручную выбрать файл конфигурации SSH?\",\n  \"sshConfigNoServers\": \"Серверы не найдены в SSH-конфигурации\",\n  \"sshConfigPermissionDenied\": \"Невозможно получить доступ к файлу конфигурации SSH из-за разрешений macOS.\",\n  \"sshConfigServersToImport\": \"{importCount} серверов будут импортированы\",\n  \"sshTermHelp\": \"Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.\",\n  \"sshTip\": \"Эта функция находится в стадии тестирования.\\n\\nПожалуйста, отправляйте отчеты о проблемах на {url} или присоединяйтесь к нашей разработке.\",\n  \"sshVirtualKeyAutoOff\": \"Автоматическое переключение виртуальных клавиш\",\n  \"stat\": \"Статистика\",\n  \"supportFmtArgs\": \"Поддерживаются следующие форматы аргументов:\",\n  \"suspendTip\": \"Функция приостановки требует прав root и поддержки systemd.\",\n  \"switchTo\": \"Переключиться на {val}\",\n  \"syncTip\": \"Возможно, потребуется перезагрузка, чтобы некоторые изменения вступили в силу.\",\n  \"system\": \"Система\",\n  \"tag\": \"Теги\",\n  \"tapToStartDiscovery\": \"Нажмите кнопку поиска, чтобы обнаружить SSH серверы в вашей сети\",\n  \"termFontSizeTip\": \"Эта настройка повлияет на размер терминала (ширина и высота). Вы можете масштабировать страницу терминала, чтобы调整 размер шрифта текущей сессии.\",\n  \"textScaler\": \"Масштабирование текста\",\n  \"textScalerTip\": \"1.0 => 100% (исходный размер), применяется только к части шрифтов на странице сервера, изменение не рекомендуется.\",\n  \"time\": \"Время\",\n  \"times\": \"Раз\",\n  \"trySudo\": \"Попробовать использовать sudo\",\n  \"unknown\": \"Неизвестно\",\n  \"unkownConvertMode\": \"Неизвестный режим конвертации\",\n  \"update\": \"Обновление\",\n  \"updateIntervalEqual0\": \"Если установлено 0, статус сервера не будет автоматически обновляться.\\nТакже не будет рассчитано использование ЦП.\",\n  \"updateServerStatusInterval\": \"Интервал обновления статуса сервера\",\n  \"upsideDown\": \"Перевернуть\",\n  \"useCdn\": \"Использование CDN\",\n  \"useCdnTip\": \"Не китайским пользователям рекомендуется использовать CDN. Хотели бы вы его использовать?\",\n  \"useNoPwd\": \"Будет использоваться без пароля\",\n  \"usePodmanByDefault\": \"Использовать Podman по умолчанию\",\n  \"used\": \"Использовано\",\n  \"view\": \"Вид\",\n  \"viewDetails\": \"Просмотр деталей\",\n  \"viewErr\": \"Просмотр ошибок\",\n  \"virtKeyHelpClipboard\": \"Если в терминале выделен текст, то он копируется в буфер обмена, в противном случае содержимое буфера вставляется в терминал.\",\n  \"virtKeyHelpIME\": \"Включить/выключить клавиатуру\",\n  \"virtKeyHelpSFTP\": \"Открыть текущий путь в SFTP.\",\n  \"waitConnection\": \"Пожалуйста, дождитесь установки соединения\",\n  \"wakeLock\": \"Держать включенным\",\n  \"watchNotPaired\": \"Apple Watch не сопряжены\",\n  \"webdavSettingEmpty\": \"Настройки Webdav пусты\",\n  \"whenOpenApp\": \"При открытии приложения\",\n  \"wolTip\": \"После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.\",\n  \"write\": \"Запись\",\n  \"writeScriptFailTip\": \"Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.\",\n  \"writeScriptTip\": \"После подключения к серверу скрипт будет записан в `~/.config/server_box` \\n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.\",\n  \"podmanDockerEmulationDetected\": \"Обнаружена эмуляция Podman Docker. Пожалуйста, переключитесь на Podman в настройках.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_tr.arb",
    "content": "{\n  \"@@locale\": \"tr\",\n  \"aboutThanks\": \"Aşağıdaki katılımcılara teşekkürler.\",\n  \"acceptBeta\": \"Beta sürüm güncellemelerini kabul et\",\n  \"addSystemPrivateKeyTip\": \"Şu anda özel anahtarlar mevcut değil, sistemle birlikte gelen anahtarı (~/.ssh/id_rsa) eklemek ister misiniz?\",\n  \"added2List\": \"Görev listesine eklendi\",\n  \"addr\": \"Adres\",\n  \"alreadyLastDir\": \"Zaten son dizindesiniz.\",\n  \"askAi\": \"Yapay zekaya sor\",\n  \"askAiApiKey\": \"API anahtarı\",\n  \"askAiAwaitingResponse\": \"Yapay zekâ yanıtı bekleniyor...\",\n  \"askAiBaseUrl\": \"Temel URL\",\n  \"askAiCommandInserted\": \"Komut terminale eklendi\",\n  \"askAiConfigMissing\": \"Lütfen Ayarlar'da {fields} öğesini yapılandırın.\",\n  \"askAiConfirmExecute\": \"Çalıştırmadan önce onayla\",\n  \"askAiConversation\": \"YZ sohbeti\",\n  \"askAiDisclaimer\": \"Yapay zeka hata yapabilir. Lütfen dikkatli kullanın.\",\n  \"askAiFollowUpHint\": \"Yeni bir soru sor...\",\n  \"askAiInsertTerminal\": \"Terminale ekle\",\n  \"askAiNoResponse\": \"Yanıt yok\",\n  \"askAiRecommendedCommand\": \"YZ önerilen komut\",\n  \"askAiSelectedContent\": \"Seçilen içerik\",\n  \"askAiUsageHint\": \"SSH Terminalinde kullanılır\",\n  \"atLeastOneTab\": \"En az bir sekme seçilmelidir\",\n  \"authFailTip\": \"Kimlik doğrulama başarısız oldu, lütfen kimlik bilgilerinin doğru olup olmadığını kontrol edin\",\n  \"autoBackupConflict\": \"Aynı anda yalnızca bir otomatik yedekleme açık olabilir.\",\n  \"autoConnect\": \"Otomatik bağlan\",\n  \"autoRun\": \"Otomatik çalıştır\",\n  \"autoUpdateHomeWidget\": \"Ana ekran bileşenini otomatik güncelle\",\n  \"availableTabs\": \"Mevcut Sekmeler\",\n  \"backupEncrypted\": \"Yedekleme şifrelenmiş\",\n  \"backupNotEncrypted\": \"Yedekleme şifreli değil\",\n  \"backupPassword\": \"Yedekleme parolası\",\n  \"backupPasswordRemoved\": \"Yedekleme parolası kaldırıldı\",\n  \"backupPasswordSet\": \"Yedekleme parolası ayarlandı\",\n  \"backupPasswordTip\": \"Yedekleme dosyalarını şifrelemek için bir parola belirleyin. Şifrelemeyi devre dışı bırakmak için boş bırakın.\",\n  \"backupPasswordWrong\": \"Yanlış yedekleme parolası\",\n  \"backupTip\": \"Dışa aktarılan veriler parola ile şifrelenebilir. \\nLütfen güvenli bir şekilde saklayın.\",\n  \"backupVersionNotMatch\": \"Yedekleme sürümü eşleşmiyor.\",\n  \"bgRun\": \"Arka planda çalıştır\",\n  \"bgRunTip\": \"Bu anahtar yalnızca programın arka planda çalışmayı deneyeceği anlamına gelir. Arka planda çalışıp çalışamayacağı, iznin etkinleştirilip etkinleştirilmediğine bağlıdır. AOSP tabanlı Android ROM'lar için lütfen bu uygulamada \\\"Pil Optimizasyonu\\\"nu devre dışı bırakın. MIUI / HyperOS için lütfen güç tasarrufu politikasını \\\"Sınırsız\\\" olarak değiştirin.\",\n  \"clearAllStatsContent\": \"Tüm sunucu bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.\",\n  \"clearAllStatsTitle\": \"Tüm İstatistikleri Temizle\",\n  \"clearServerStatsContent\": \"\\\"{serverName}\\\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.\",\n  \"clearServerStatsTitle\": \"{serverName} İstatistiklerini Temizle\",\n  \"clearThisServerStats\": \"Bu Sunucu İstatistiklerini Temizle\",\n  \"compactDatabase\": \"Veritabanını Sıkıştır\",\n  \"compactDatabaseContent\": \"Veritabanı boyutu: {size}\\n\\nBu, dosya boyutunu küçültmek için veritabanını yeniden düzenleyecektir. Veriler silinmeyecek.\",\n  \"closeAfterSave\": \"Kaydet ve kapat\",\n  \"collapseUITip\": \"Arayüzde uzun listelerin varsayılan olarak daraltılıp daraltılmayacağı\",\n  \"connectionDetails\": \"Bağlantı Detayları\",\n  \"connectionStats\": \"Bağlantı İstatistikleri\",\n  \"connectionStatsDesc\": \"Sunucu bağlantı başarı oranını ve geçmişi görüntüle\",\n  \"containerTrySudoTip\": \"Örneğin: Uygulamada kullanıcı aaa olarak ayarlanmış, ancak Docker root kullanıcısı altında kurulmuş. Bu durumda bu seçeneği etkinleştirmeniz gerekir.\",\n  \"containerSudoPasswordRequired\": \"Docker'e erişmek için sudo şifresi gereklidir. Lütfen şifrenizi girin.\",\n  \"containerSudoPasswordIncorrect\": \"Sudo şifresi yanlış veya izin verilmiyor. Lütfen tekrar deneyin.\",\n  \"convert\": \"Dönüştür\",\n  \"copyPath\": \"Yolu kopyala\",\n  \"cpuViewAsProgressTip\": \"Her CPU'nun kullanımını ilerleme çubuğu tarzında göster (eski tarz)\",\n  \"cursorType\": \"İmleç türü\",\n  \"customCmd\": \"Özel komutlar\",\n  \"customCmdHint\": \"\\\"Komut Adı\\\": \\\"Komut\\\"\",\n  \"deleteServers\": \"Sunucuları toplu sil\",\n  \"desktopTerminalTip\": \"SSH oturumları başlatılırken terminal öykünücüsünü açmak için kullanılan komut.\",\n  \"dirEmpty\": \"Klasörün boş olduğundan emin olun.\",\n  \"discoverSshServers\": \"SSH Sunucularını Keşfet\",\n  \"discoveryFailed\": \"Keşif başarısız\",\n  \"discoverySettings\": \"Keşif Ayarları\",\n  \"discoverySummary\": \"Keşif Özeti\",\n  \"diskHealth\": \"Disk sağlığı\",\n  \"diskIgnorePath\": \"Disk için yok sayılan yol\",\n  \"displayCpuIndex\": \"CPU indeksini göster\",\n  \"dl2Local\": \"{fileName} dosyasını yerel cihaza indir?\",\n  \"dockerEmptyRunningItems\": \"Çalışan konteyner yok.\\nBunun nedeni şunlar olabilir:\\n- Docker kurulum kullanıcısı, uygulamada yapılandırılan kullanıcı adıyla aynı değil.\\n- DOCKER_HOST ortam değişkeni doğru okunmadı. Terminalde `echo $DOCKER_HOST` komutunu çalıştırarak kontrol edebilirsiniz.\",\n  \"dockerImagesFmt\": \"{count} görüntü\",\n  \"dockerNotInstalled\": \"Docker kurulmamış\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} çalışıyor, {stoppedCount} konteyner durdurulmuş.\",\n  \"dockerStatusRunningFmt\": \"{count} konteyner çalışıyor.\",\n  \"doubleColumnMode\": \"Çift sütun modu\",\n  \"doubleColumnTip\": \"Bu seçenek yalnızca özelliği etkinleştirir, gerçekten etkinleşip etkinleşmeyeceği cihazın genişliğine bağlıdır\",\n  \"editVirtKeys\": \"Sanal tuşları düzenle\",\n  \"editorHighlightTip\": \"Mevcut kod vurgulama performansı ideal değil ve isteğe bağlı olarak kapatılabilir.\",\n  \"enableMdns\": \"mDNS'yi Etkinleştir\",\n  \"enableMdnsDesc\": \"SSH hizmetlerini keşfetmek için mDNS/Bonjour kullan\",\n  \"envVars\": \"Ortam değişkeni\",\n  \"experimentalFeature\": \"Deneysel özellik\",\n  \"extraArgs\": \"Ek argümanlar\",\n  \"fallbackSshDest\": \"Yedek SSH hedefi\",\n  \"fdroidReleaseTip\": \"Bu uygulamayı F-Droid'den indirdiyseniz, bu seçeneği kapatmanız önerilir.\",\n  \"fgService\": \"Ön Plan Servisi\",\n  \"fgServiceTip\": \"Etkinleştirildikten sonra bazı cihaz modellerinde çökme olabilir. Devre dışı bırakmak, bazı modellerde SSH bağlantılarının arka planda sürdürülememesine neden olabilir. Lütfen sistem ayarlarında ServerBox bildirim izinlerini, arka planda çalışmayı ve otomatik uyanmayı etkinleştirin.\",\n  \"fileTooLarge\": \"'{file}' dosyası çok büyük {size}, maksimum {sizeMax}\",\n  \"finishedAt\": \"Tamamlandı:\",\n  \"followSystem\": \"Sistemi takip et\",\n  \"fontSize\": \"Yazı tipi boyutu\",\n  \"fullScreen\": \"Tam ekran modu\",\n  \"fullScreenJitter\": \"Tam ekran titreşim\",\n  \"fullScreenJitterHelp\": \"Ekran yanmasını önlemek için\",\n  \"fullScreenTip\": \"Cihaz yatay moda döndürüldüğünde tam ekran modu etkinleştirilsin mi? Bu seçenek yalnızca sunucu sekmesi için geçerlidir.\",\n  \"goBackQ\": \"Geri dön?\",\n  \"goto\": \"Git\",\n  \"hideTitleBar\": \"Başlık çubuğunu gizle\",\n  \"highlight\": \"Kod vurgulama\",\n  \"homeTabs\": \"Ana Sayfa Sekmeleri\",\n  \"homeTabsCustomizeDesc\": \"Ana sayfada görünecek sekmeleri ve sıralarını özelleştirin\",\n  \"homeWidgetUrlConfig\": \"Ana ekran bileşeni URL'sini yapılandır\",\n  \"httpFailedWithCode\": \"İstek başarısız oldu, durum kodu: {code}\",\n  \"ignoreCert\": \"Sertifikayı yok say\",\n  \"image\": \"Görüntü\",\n  \"imagesList\": \"Görüntü listesi\",\n  \"installDockerWithUrl\": \"Lütfen önce https://docs.docker.com/engine/install adresinden Docker'ı kurun.\",\n  \"invalid\": \"Geçersiz\",\n  \"jumpServer\": \"Atlama sunucusu\",\n  \"keepForeground\": \"Uygulamayı ön planda tut!\",\n  \"keepStatusWhenErr\": \"Son sunucu durumunu koru\",\n  \"keepStatusWhenErrTip\": \"Yalnızca betik yürütülmesi sırasında bir hata olduğunda\",\n  \"keyAuth\": \"Anahtar Kimlik Doğrulama\",\n  \"lastFailure\": \"Son Başarısızlık\",\n  \"lastSuccess\": \"Son Başarı\",\n  \"letterCache\": \"Harf önbelleği\",\n  \"letterCacheTip\": \"Devre dışı bırakılması önerilir, ancak devre dışı bırakıldığında CJK karakterlerini girmek mümkün olmayacaktır.\",\n  \"madeWithLove\": \"{myGithub} tarafından ❤️ ile yapıldı\",\n  \"max\": \"maks\",\n  \"maxConcurrency\": \"Maksimum Eşzamanlılık\",\n  \"maxRetryCount\": \"Sunucu yeniden bağlantı sayısı\",\n  \"maxRetryCountEqual0\": \"Tekrar tekrar deneyecek.\",\n  \"min\": \"min\",\n  \"more\": \"Daha fazla\",\n  \"moveOutServerFuncBtnsHelp\": \"Açık: Sunucu Sekmesi sayfasındaki her kartın altında görüntülenebilir. Kapalı: Sunucu Ayrıntıları sayfasının üstünde görüntülenebilir.\",\n  \"needHomeDir\": \"Synology kullanıcısıysanız, [buraya bakın](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Diğer sistem kullanıcılarının bir ana dizin oluşturma yöntemini araması gerekir.\",\n  \"needRestart\": \"Uygulamanın yeniden başlatılması gerekiyor\",\n  \"netViewType\": \"Ağ görüntüleme türü\",\n  \"newContainer\": \"Yeni konteyner\",\n  \"noConnectionStatsData\": \"Bağlantı istatistik verisi yok\",\n  \"noLineChart\": \"Çizgi grafikleri kullanma\",\n  \"noLineChartForCpu\": \"CPU için çizgi grafikleri kullanma\",\n  \"noPrivateKeyTip\": \"Özel anahtar mevcut değil, silinmiş olabilir veya yapılandırma hatası vardır.\",\n  \"noPromptAgain\": \"Tekrar sorma\",\n  \"onServerDetailPage\": \"Sunucu ayrıntı sayfasında\",\n  \"onlyOneLine\": \"Yalnızca tek satır olarak göster (kaydırılabilir)\",\n  \"onlyWhenCoreBiggerThan8\": \"Çekirdek sayısı 8'den büyük olduğunda çalışır\",\n  \"openLastPath\": \"Son yolu aç\",\n  \"openLastPathTip\": \"Farklı sunucular farklı günlükler tutar ve günlük, çıkış yoludur\",\n  \"parseContainerStatsTip\": \"Docker'ın doluluk durumunu ayrıştırmak oldukça yavaş.\",\n  \"percentOfSize\": \"{size}'ın {percent}%'i\",\n  \"permission\": \"İzinler\",\n  \"pingInputIP\": \"Lütfen bir hedef IP / alan adı girin.\",\n  \"pingNoServer\": \"Ping yapılacak sunucu yok.\\nLütfen sunucu sekmesinde bir sunucu ekleyin.\",\n  \"plugInType\": \"Eklenti Türü\",\n  \"preferDiskAmount\": \"Disk kapasitesini öncelikli olarak göster\",\n  \"privateKey\": \"Özel Anahtar\",\n  \"privateKeyNotFoundFmt\": \"Özel anahtar [{keyId}] bulunamadı.\",\n  \"pushToken\": \"Push belirteci\",\n  \"pveIgnoreCertTip\": \"Etkinleştirilmesi önerilmez, güvenlik risklerine dikkat edin! PVE'den varsayılan sertifikayı kullanıyorsanız, bu seçeneği etkinleştirmeniz gerekir.\",\n  \"pveLoginFailed\": \"Giriş başarısız. Linux PAM girişi için sunucu yapılandırmasındaki kullanıcı adı/şifre ile kimlik doğrulama yapılamadı.\",\n  \"pveVersionLow\": \"Bu özellik şu anda test aşamasında ve yalnızca PVE 8+ üzerinde test edildi. Lütfen dikkatli kullanın.\",\n  \"read\": \"Oku\",\n  \"recentConnections\": \"Son Bağlantılar\",\n  \"rememberPwdInMem\": \"Şifreyi bellekte hatırla\",\n  \"rememberPwdInMemTip\": \"Konteynerler, askıya alma vb. için kullanılır.\",\n  \"rememberWindowSize\": \"Pencere boyutunu hatırla\",\n  \"remotePath\": \"Uzak yol\",\n  \"result\": \"Sonuç\",\n  \"rotateAngel\": \"Dönüş açısı\",\n  \"sameIdServerExist\": \"Aynı kimliğe sahip bir sunucu zaten mevcut\",\n  \"save\": \"Kaydet\",\n  \"second\": \"s\",\n  \"serverDetailOrder\": \"Ayrıntı sayfası bileşen sırası\",\n  \"serverFuncBtns\": \"Sunucu işlev düğmeleri\",\n  \"serverOrder\": \"Sunucu sırası\",\n  \"serverTabRequired\": \"Sunucu sekmesi kaldırılamaz\",\n  \"sftpDlPrepare\": \"Bağlantı hazırlanıyor...\",\n  \"sftpEditorTip\": \"Boşsa, uygulamanın yerleşik dosya düzenleyicisi kullanılır. Bir değer varsa, uzak sunucunun düzenleyicisi kullanılır, örn. `vim` (otomatik olarak `EDITOR`'a göre algılanması önerilir).\",\n  \"sftpRmrDirSummary\": \"SFTP'de bir klasörü silmek için `rm -r` kullan.\",\n  \"sftpSSHConnected\": \"SFTP Bağlandı\",\n  \"sftpShowFoldersFirst\": \"Önce klasörleri göster\",\n  \"showDistLogo\": \"Dağıtım logosunu göster\",\n  \"size\": \"Boyut\",\n  \"softWrap\": \"Yumuşak kaydırma\",\n  \"specifyDev\": \"Cihazı belirt\",\n  \"specifyDevTip\": \"Örneğin, ağ trafiği istatistikleri varsayılan olarak tüm cihazlar içindir. Burada belirli bir cihaz belirtebilirsiniz.\",\n  \"speed\": \"Hız\",\n  \"spentTime\": \"Harcanan süre: {time}\",\n  \"sshConfigAllExist\": \"Tüm sunucular zaten mevcut ({duplicateCount} kopya bulundu)\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} kopya atlanacak\",\n  \"sshConfigFound\": \"Sisteminizde SSH yapılandırması bulduk\",\n  \"sshConfigFoundServers\": \"{totalCount} sunucu bulundu\",\n  \"sshConfigImport\": \"SSH Yapılandırma İçe Aktarma\",\n  \"sshConfigImportHelp\": \"Yalnızca temel bilgiler içe aktarılabilir, örneğin: IP/Port.\",\n  \"sshConfigImportPermission\": \"~/.ssh/config dosyasını okumak ve sunucu ayarlarını otomatik olarak içe aktarmak için izin vermek ister misiniz?\",\n  \"sshConfigImportTip\": \"İlk sunucu oluşturulurken ~/.ssh/config okuma istemi\",\n  \"sshConfigImported\": \"SSH yapılandırmasından {count} sunucu içe aktarıldı\",\n  \"sshHostKeyChangedDesc\": \"{serverName} için SSH ana bilgisayar anahtarı değişti. Yalnızca bu sunucuya güveniyorsanız devam edin.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Parmak izi (MD5 Base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Parmak izi (MD5 hex): {fingerprint}\",\n  \"sshHostKeyType\": \"SSH ana bilgisayar anahtarı türü\",\n  \"sshHostKeyNewDesc\": \"{serverName} üzerinden yeni bir SSH ana bilgisayar anahtarı alındı. Güvenmeden önce parmak izini kontrol edin.\",\n  \"sshHostKeyStoredFingerprint\": \"Kaydedilen parmak izi: {fingerprint}\",\n  \"sshConfigManualSelect\": \"SSH yapılandırma dosyasını manuel olarak seçmek ister misiniz?\",\n  \"sshConfigNoServers\": \"SSH yapılandırmasında sunucu bulunamadı\",\n  \"sshConfigPermissionDenied\": \"macOS izinleri nedeniyle SSH yapılandırma dosyasına erişilemiyor.\",\n  \"sshConfigServersToImport\": \"{importCount} sunucu içe aktarılacak\",\n  \"sshTermHelp\": \"Terminal kaydırılabilir olduğunda, yatay olarak sürüklemek metni seçebilir. Klavye düğmesine tıklamak klavyeyi açar/kapar. Dosya simgesi mevcut yolu SFTP'de açar. Pano düğmesi, metin seçiliyken içeriği kopyalar ve metin seçili değilken panoda içerik varsa terminale yapıştırır. Kod simgesi, kod parçacıklarını terminale yapıştırır ve yürütür.\",\n  \"sshTip\": \"Bu işlev şu anda deneysel aşamada.\\n\\nLütfen hataları {url} adresinde bildirin veya geliştirmemize katılın.\",\n  \"sshVirtualKeyAutoOff\": \"Sanal tuşların otomatik geçişi\",\n  \"stat\": \"İstatistik\",\n  \"supportFmtArgs\": \"Aşağıdaki biçimlendirme parametreleri desteklenir:\",\n  \"suspendTip\": \"Askıya alma işlevi, root izni ve systemd desteği gerektirir.\",\n  \"switchTo\": \"{val}'a geç\",\n  \"syncTip\": \"Bazı değişikliklerin etkili olması için yeniden başlatma gerekebilir.\",\n  \"system\": \"Sistem\",\n  \"tag\": \"Etiketler\",\n  \"tapToStartDiscovery\": \"Ağınızdaki SSH sunucularını keşfetmek için arama düğmesine dokunun\",\n  \"termFontSizeTip\": \"Bu ayar terminal boyutunu (genişlik ve yükseklik) etkiler. Terminal sayfasında yakınlaştırarak mevcut oturumun yazı tipi boyutunu ayarlayabilirsiniz.\",\n  \"textScaler\": \"Metin ölçekleyici\",\n  \"textScalerTip\": \"1.0 => %100 (orijinal boyut), yalnızca sunucu sayfasındaki yazı tipinin bir kısmı üzerinde çalışır, değiştirilmesi önerilmez.\",\n  \"time\": \"Zaman\",\n  \"times\": \"Kez\",\n  \"trySudo\": \"Sudo ile dene\",\n  \"unknown\": \"Bilinmeyen\",\n  \"unkownConvertMode\": \"Bilinmeyen dönüşüm modu\",\n  \"update\": \"Güncelle\",\n  \"updateIntervalEqual0\": \"0 olarak ayarladınız, otomatik güncelleme yapılmayacak.\\nCPU durumu hesaplanamaz.\",\n  \"updateServerStatusInterval\": \"Sunucu durumu güncelleme aralığı\",\n  \"upsideDown\": \"Başaşağı\",\n  \"useCdn\": \"CDN kullan\",\n  \"useCdnTip\": \"Çinli olmayan kullanıcıların CDN kullanması önerilir. Kullanmak ister misiniz?\",\n  \"useNoPwd\": \"Şifre kullanılmayacak\",\n  \"usePodmanByDefault\": \"Varsayılan olarak Podman kullan\",\n  \"used\": \"Kullanılan\",\n  \"view\": \"Görünüm\",\n  \"viewDetails\": \"Detayları Görüntüle\",\n  \"viewErr\": \"Hatayı gör\",\n  \"virtKeyHelpClipboard\": \"Seçili terminal boş değilse panoya kopyala, aksi takdirde panodaki içeriği terminale yapıştır.\",\n  \"virtKeyHelpIME\": \"Klavyeyi aç/kapat\",\n  \"virtKeyHelpSFTP\": \"Mevcut dizini SFTP'de aç.\",\n  \"waitConnection\": \"Lütfen bağlantının kurulmasını bekleyin.\",\n  \"wakeLock\": \"Uyanık tut\",\n  \"watchNotPaired\": \"Eşleştirilmiş Apple Watch yok\",\n  \"webdavSettingEmpty\": \"WebDav ayarı boş\",\n  \"whenOpenApp\": \"Uygulama açıldığında\",\n  \"wolTip\": \"WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.\",\n  \"write\": \"Yaz\",\n  \"writeScriptFailTip\": \"Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.\",\n  \"writeScriptTip\": \"Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \\n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.\",\n  \"podmanDockerEmulationDetected\": \"Podman Docker emülasyonu tespit edildi. Lütfen ayarlarda Podman'a geçin.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_uk.arb",
    "content": "{\n  \"@@locale\": \"uk\",\n  \"aboutThanks\": \"Дякуємо наступним особам, які взяли участь.\",\n  \"acceptBeta\": \"Прийняти оновлення бета-версії\",\n  \"addSystemPrivateKeyTip\": \"Наразі приватних ключів нема, хочете додати той, що йде з системою (~/.ssh/id_rsa)?\",\n  \"added2List\": \"Додано до списку завдань\",\n  \"addr\": \"Адреса\",\n  \"alreadyLastDir\": \"Вже в останньому каталозі.\",\n  \"askAi\": \"Запитати ШІ\",\n  \"askAiApiKey\": \"Ключ API\",\n  \"askAiAwaitingResponse\": \"Очікування відповіді ШІ...\",\n  \"askAiBaseUrl\": \"Базова URL\",\n  \"askAiCommandInserted\": \"Команду вставлено в термінал\",\n  \"askAiConfigMissing\": \"Налаштуйте {fields} у налаштуваннях.\",\n  \"askAiConfirmExecute\": \"Підтвердити перед виконанням\",\n  \"askAiConversation\": \"Розмова з ШІ\",\n  \"askAiDisclaimer\": \"ШІ може помилятися. Користуйтеся обережно.\",\n  \"askAiFollowUpHint\": \"Поставте додаткове запитання...\",\n  \"askAiInsertTerminal\": \"Вставити в термінал\",\n  \"askAiNoResponse\": \"Відповідь відсутня\",\n  \"askAiRecommendedCommand\": \"Команда, запропонована ШІ\",\n  \"askAiSelectedContent\": \"Вибраний вміст\",\n  \"askAiUsageHint\": \"Використовується в SSH-терміналі\",\n  \"atLeastOneTab\": \"Потрібно вибрати принаймні одну вкладку\",\n  \"authFailTip\": \"Авторизація не вдалася, будь ласка, перевірте правильність облікових даних\",\n  \"autoBackupConflict\": \"Тільки одне автоматичне резервне копіювання може бути активне одночасно.\",\n  \"autoConnect\": \"Авто підключення\",\n  \"autoRun\": \"Авто запуск\",\n  \"autoUpdateHomeWidget\": \"Автоматичне оновлення віджетів на головному екрані\",\n  \"availableTabs\": \"Доступні вкладки\",\n  \"backupEncrypted\": \"Резервна копія зашифрована\",\n  \"backupNotEncrypted\": \"Резервна копія не зашифрована\",\n  \"backupPassword\": \"Пароль резервного копіювання\",\n  \"backupPasswordRemoved\": \"Пароль резервного копіювання видалено\",\n  \"backupPasswordSet\": \"Пароль резервного копіювання встановлено\",\n  \"backupPasswordTip\": \"Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.\",\n  \"backupPasswordWrong\": \"Неправильний пароль резервного копіювання\",\n  \"backupTip\": \"Експортовані дані можуть бути зашифровані паролем. \\nБудь ласка, зберігайте їх у безпеці.\",\n  \"backupVersionNotMatch\": \"Версія резервного копіювання не збіглася.\",\n  \"bgRun\": \"Запуск у фоновому режимі\",\n  \"bgRunTip\": \"Цей перемикач лише вказує на те, що програма намагатиметься працювати у фоновому режимі. Чи може вона працювати у фоновому режимі, залежить від прав доступу. Для AOSP-орієнтованих Android ROM, будь ласка, вимкніть \\\"Оптимізацію акумулятора\\\" в цьому додатку. Для MIUI / HyperOS, будь ласка, змініть політику економії енергії на \\\"Нескінченна\\\".\",\n  \"clearAllStatsContent\": \"Ви впевнені, що хочете очистити всю статистику з'єднань сервера? Цю дію не можна скасувати.\",\n  \"clearAllStatsTitle\": \"Очистити всю статистику\",\n  \"clearServerStatsContent\": \"Ви впевнені, що хочете очистити статистику з'єднань для сервера \\\"{serverName}\\\"? Цю дію не можна скасувати.\",\n  \"clearServerStatsTitle\": \"Очистити статистику {serverName}\",\n  \"clearThisServerStats\": \"Очистити статистику цього сервера\",\n  \"compactDatabase\": \"Стиснути базу даних\",\n  \"compactDatabaseContent\": \"Розмір бази даних: {size}\\n\\nЦе перебудує базу даних, щоб зменшити розмір файлу. Дані не будуть видалені.\",\n  \"closeAfterSave\": \"Зберегти та закрити\",\n  \"collapseUITip\": \"Сховати довгі списки, що є у UI за замовчуванням\",\n  \"connectionDetails\": \"Деталі з'єднання\",\n  \"connectionStats\": \"Статистика з'єднань\",\n  \"connectionStatsDesc\": \"Переглянути коефіцієнт успішності підключення до сервера та історію\",\n  \"containerTrySudoTip\": \"Наприклад: У застосунку користувач це aaa, але Docker встановлений під користувачем root. У цьому випадку вам потрібно активувати цю опцію.\",\n  \"containerSudoPasswordRequired\": \"Для доступу до Docker потрібен пароль sudo. Будь ласка, введіть ваш пароль.\",\n  \"containerSudoPasswordIncorrect\": \"Пароль sudo неправильний або не дозволений. Будь ласка, спробуйте ще раз.\",\n  \"convert\": \"Конвертувати\",\n  \"copyPath\": \"Скопіювати шлях\",\n  \"cpuViewAsProgressTip\": \"Відобразити використання кожного процесора у вигляді стовпчикової діаграми (старий стиль)\",\n  \"cursorType\": \"Тип курсора\",\n  \"customCmd\": \"Користувацькі команди\",\n  \"customCmdHint\": \"\\\"Ім'я Команди\\\": \\\"Команда\\\"\",\n  \"deleteServers\": \"Масове видалення серверів\",\n  \"desktopTerminalTip\": \"Команда для відкриття емулятора термінала під час запуску SSH-сеансів.\",\n  \"dirEmpty\": \"Переконайтеся, що директорія пуста.\",\n  \"discoverSshServers\": \"Виявити SSH сервери\",\n  \"discoveryFailed\": \"Виявлення не вдалось\",\n  \"discoverySettings\": \"Налаштування виявлення\",\n  \"discoverySummary\": \"Підсумок виявлення\",\n  \"diskHealth\": \"Стан диска\",\n  \"diskIgnorePath\": \"Ігнорувати шлях для диска\",\n  \"displayCpuIndex\": \"Відобразити індекс ЦП\",\n  \"dl2Local\": \"Завантажити {fileName} на локальний комп'ютер?\",\n  \"dockerEmptyRunningItems\": \"Немає запущених контейнерів.\\nЦе може бути через:\\n- Користувача Docker, відмінного від користувача, налаштованого в додатку\\n- змінну оточення DOCKER_HOST, яка не була правильно зчитана. Ви можете виконати `echo $DOCKER_HOST` у терміналі, щоб побачити її значення.\",\n  \"dockerImagesFmt\": \"Всього {count} образів\",\n  \"dockerNotInstalled\": \"Docker не встановлено\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} запущено, {stoppedCount} контейнерів зупинено.\",\n  \"dockerStatusRunningFmt\": \"{count} контейнер(и) запущено.\",\n  \"doubleColumnMode\": \"Режим подвійної колонки\",\n  \"doubleColumnTip\": \"Ця опція лише активує функцію, чи можна її насправді включити, залежить від ширини пристрою\",\n  \"editVirtKeys\": \"Редагувати віртуальні клавіші\",\n  \"editorHighlightTip\": \"Поточна підсвітка коду не ідеальна і може бути вимкнена для покращення.\",\n  \"enableMdns\": \"Увімкнути mDNS\",\n  \"enableMdnsDesc\": \"Використовувати mDNS/Bonjour для виявлення SSH сервісів\",\n  \"envVars\": \"Змінні середовища\",\n  \"experimentalFeature\": \"Експериментальна функція\",\n  \"extraArgs\": \"Додаткові аргументи\",\n  \"fallbackSshDest\": \"Резервна SSH адреса\",\n  \"fdroidReleaseTip\": \"Якщо ви завантажили цей застосунок з F-Droid, рекомендується відключити цю опцію.\",\n  \"fgService\": \"Служба переднього плану\",\n  \"fgServiceTip\": \"Після увімкнення деякі моделі пристроїв можуть вилітати. Вимкнення може призвести до того, що деякі моделі не зможуть підтримувати SSH-з'єднання у фоновому режимі. Будь ласка, дозвольте ServerBox права на сповіщення, фонову роботу та самопробудження в системних налаштуваннях.\",\n  \"fileTooLarge\": \"Файл '{file}' занадто великий ({size}), макс {sizeMax}\",\n  \"finishedAt\": \"Завершено о\",\n  \"followSystem\": \"Слідувати системі\",\n  \"fontSize\": \"Розмір шрифту\",\n  \"fullScreen\": \"Повноекранний режим\",\n  \"fullScreenJitter\": \"Тремтіння в повноекранному режимі\",\n  \"fullScreenJitterHelp\": \"Щоб уникнути вигоряння екрану\",\n  \"fullScreenTip\": \"Чи слід увімкнути повноекранний режим під час повороту пристрою в горизонтальне положення? Ця опція стосується лише вкладки сервера.\",\n  \"goBackQ\": \"Повернутися назад?\",\n  \"goto\": \"Перейти до\",\n  \"hideTitleBar\": \"Сховати заголовок\",\n  \"highlight\": \"Підсвітка коду\",\n  \"homeTabs\": \"Домашні вкладки\",\n  \"homeTabsCustomizeDesc\": \"Налаштуйте, які вкладки відображаються на головній сторінці та їх порядок\",\n  \"homeWidgetUrlConfig\": \"Налаштувати URL віджета на головному екрані\",\n  \"httpFailedWithCode\": \"Запит не вдався, код статусу: {code}\",\n  \"ignoreCert\": \"Ігнорувати сертифікат\",\n  \"image\": \"Зображення\",\n  \"imagesList\": \"Список зображень\",\n  \"installDockerWithUrl\": \"Будь ласка, спочатку встановіть Docker. (https://docs.docker.com/engine/install)\",\n  \"invalid\": \"Недійсний\",\n  \"invalidHostFormat\": \"Недійсний формат хоста. Дозволено лише символи IPv4, IPv6 та домену.\",\n  \"jumpServer\": \"Стрибковий Сервер\",\n  \"keepForeground\": \"Тримати застосунок на передньому плані!\",\n  \"keepStatusWhenErr\": \"Зберегати останній стан сервера\",\n  \"keepStatusWhenErrTip\": \"Тільки в разі виникнення помилки під час виконання скрипту\",\n  \"keyAuth\": \"Аутентифікація ключем\",\n  \"lastFailure\": \"Остання помилка\",\n  \"lastSuccess\": \"Останній успіх\",\n  \"letterCache\": \"Кешування букв\",\n  \"letterCacheTip\": \"Рекомендується відключити, але після вимкнення стане неможливим введення CJK (китайських, японських, корейських) символів.\",\n  \"madeWithLove\": \"Зроблено з ❤️ від {myGithub}\",\n  \"max\": \"макс.\",\n  \"maxConcurrency\": \"Максимальна паралельність\",\n  \"maxRetryCount\": \"Кількість повторних спроб підключення до сервера\",\n  \"maxRetryCountEqual0\": \"Знову і знову буде намагатися повторно підключитися.\",\n  \"min\": \"мін.\",\n  \"more\": \"Більше\",\n  \"moveOutServerFuncBtnsHelp\": \"Включено: може відображатися під кожною карткою на вкладці Сервер. Вимкнено: може відображатися вгорі на сторінці деталей сервера.\",\n  \"needHomeDir\": \"Якщо ви користувач Synology, [дивіться тут](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Користувачі інших систем повинні знайти інформацію про те, як створити домашній каталог.\",\n  \"needRestart\": \"Необхідно перезапустити застосунок\",\n  \"netViewType\": \"Тип перегляду мережі\",\n  \"newContainer\": \"Новий контейнер\",\n  \"noConnectionStatsData\": \"Немає даних статистики з'єднань\",\n  \"noLineChart\": \"Не використовувати лінійні діаграми\",\n  \"noLineChartForCpu\": \"Не використовувати лінійні діаграми для ЦП\",\n  \"noPrivateKeyTip\": \"Приватного ключа немає, можливо, він був видалений або сталася помилка конфігурації.\",\n  \"noPromptAgain\": \"Більше не запитувати\",\n  \"onServerDetailPage\": \"На сторінці деталі сервера\",\n  \"onlyOneLine\": \"Відображати лише в один рядок (прокрутка)\",\n  \"onlyWhenCoreBiggerThan8\": \"Працює лише тоді, коли кількість ядер перевищує 8\",\n  \"openLastPath\": \"Відкрити останній шлях\",\n  \"openLastPathTip\": \"Для різних серверів будуть збережені різні логи. Записується шлях при виході\",\n  \"parseContainerStatsTip\": \"Парсинг статусу зайнятості Docker є відносно повільним.\",\n  \"percentOfSize\": \"{percent}% з {size}\",\n  \"permission\": \"Дозволи\",\n  \"pingInputIP\": \"Будь ласка, введіть цільовий IP / Домен.\",\n  \"pingNoServer\": \"Немає сервера для пінгування.\\nБудь ласка, додайте сервер у вкладці `Сервер`.\",\n  \"plugInType\": \"Тип вставки\",\n  \"preferDiskAmount\": \"Пріоритетно показувати ємність диска\",\n  \"privateKey\": \"Приватний ключ\",\n  \"privateKeyNotFoundFmt\": \"Приватний ключ [{keyId}] не знайдено.\",\n  \"pushToken\": \"Надіслати токен\",\n  \"pveIgnoreCertTip\": \"Не рекомендується включати, будьте обережні з ризиками безпеки! Якщо ви використовуєте стандартний сертифікат від PVE, вам потрібно увімкнути цю опцію.\",\n  \"pveLoginFailed\": \"Не вдалося увійти. Неможливо пройти аутентифікацію за допомогою імені користувача/пароля з конфігурації сервера для входу Linux PAM.\",\n  \"pveVersionLow\": \"Ця функція наразі перебуває на стадії тестування та випробувалася лише на PVE 8+. Будь ласка, використовуйте її з обережністю.\",\n  \"read\": \"Читати\",\n  \"recentConnections\": \"Останні з'єднання\",\n  \"rememberPwdInMem\": \"Запам'ятати пароль у пам'яті\",\n  \"rememberPwdInMemTip\": \"Використовується для контейнерів, призупинення тощо.\",\n  \"rememberWindowSize\": \"Запам'ятати розмір вікна\",\n  \"remotePath\": \"Віддалений шлях\",\n  \"result\": \"Результат\",\n  \"rotateAngel\": \"Кут повороту\",\n  \"sameIdServerExist\": \"Сервер з таким ID вже існує\",\n  \"save\": \"Зберегти\",\n  \"second\": \"сек.\",\n  \"serverDetailOrder\": \"Порядок віджетів на сторінці деталі\",\n  \"serverFuncBtns\": \"Кнопки функцій сервера\",\n  \"serverOrder\": \"Порядок сервера\",\n  \"serverTabRequired\": \"Вкладку сервера не можна видалити\",\n  \"sftpDlPrepare\": \"Підготовка до підключення...\",\n  \"sftpEditorTip\": \"Якщо порожньо, використовуйте вбудований редактор файлів програми. Якщо є значення, використовуйте редактор віддаленого сервера, наприклад, `vim` (рекомендується автоматично визначити відповідно до `EDITOR`).\",\n  \"sftpRmrDirSummary\": \"Використовуйте `rm -r`, щоб видалити папку в SFTP.\",\n  \"sftpSSHConnected\": \"SFTP підключено\",\n  \"sftpShowFoldersFirst\": \"Спочатку відображати директорії\",\n  \"showDistLogo\": \"Показати логотип дистрибутива\",\n  \"size\": \"Розмір\",\n  \"softWrap\": \"М'ягкий перенос\",\n  \"specifyDev\": \"Вказати пристрій\",\n  \"specifyDevTip\": \"Наприклад, статистика мережевого трафіку за замовчуванням є для всіх пристроїв. Ви можете вказати певний пристрій тут.\",\n  \"speed\": \"Швидкість\",\n  \"spentTime\": \"Витрачений час: {time}\",\n  \"sshConfigAllExist\": \"Всі сервери вже існують (знайдено {duplicateCount} дублікатів)\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} дублікатів буде пропущено\",\n  \"sshConfigFound\": \"Ми знайшли SSH-конфігурацію у вашій системі\",\n  \"sshConfigFoundServers\": \"Знайдено {totalCount} серверів\",\n  \"sshConfigImport\": \"Імпорт SSH Конфігурації\",\n  \"sshConfigImportHelp\": \"Можна імпортувати лише базову інформацію, наприклад: IP/порт.\",\n  \"sshConfigImportPermission\": \"Чи хочете ви надати дозвіл на читання ~/.ssh/config та автоматичний імпорт налаштувань сервера?\",\n  \"sshConfigImportTip\": \"Пропозиція прочитати ~/.ssh/config при створенні першого сервера\",\n  \"sshConfigImported\": \"Імпортовано {count} серверів з SSH-конфігурації\",\n  \"sshHostKeyChangedDesc\": \"SSH-ключ хоста для {serverName} змінено. Продовжуйте лише якщо довіряєте цьому серверу.\",\n  \"sshHostKeyFingerprintMd5Base64\": \"Відбиток (MD5 Base64): {fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"Відбиток (MD5 hex): {fingerprint}\",\n  \"sshHostKeyType\": \"Тип ключа хоста SSH\",\n  \"sshHostKeyNewDesc\": \"Отримано новий SSH-ключ хоста від {serverName}. Перевірте відбиток перед тим, як довіряти.\",\n  \"sshHostKeyStoredFingerprint\": \"Збережений відбиток: {fingerprint}\",\n  \"sshConfigManualSelect\": \"Чи хочете ви вручну вибрати файл конфігурації SSH?\",\n  \"sshConfigNoServers\": \"Сервери не знайдені в SSH-конфігурації\",\n  \"sshConfigPermissionDenied\": \"Неможливо отримати доступ до файлу конфігурації SSH через дозволи macOS.\",\n  \"sshConfigServersToImport\": \"{importCount} серверів буде імпортовано\",\n  \"sshTermHelp\": \"Коли термінал прокрутний, горизонтальне проведення вибирає текст. Натискання кнопки клавіатури вмикає/вимикає клавіатуру. Іконка файлу відкриває поточний шлях SFTP. Кнопка буфера обміну копіює вміст, коли текст вибрано, і вставляє вміст з буфера обміну в термінал, коли текст не вибрано і є вміст у буфері обміну. Іконка коду вставляє фрагменти коду в термінал і виконує їх.\",\n  \"sshTip\": \"Ця функція наразі в експериментальній стадії. Будь ласка, повідомте про помилки за адресою {url} або приєднуйтеся до нашої розробки.\",\n  \"sshVirtualKeyAutoOff\": \"Автоматичне переключення віртуальних клавіш\",\n  \"stat\": \"Статистика\",\n  \"supportFmtArgs\": \"Підтримуються такі параметри форматування:\",\n  \"suspendTip\": \"Функція призупинення потребує адміністративних прав та підтримки systemd.\",\n  \"switchTo\": \"Переключитися на {val}\",\n  \"syncTip\": \"Може знадобитися перезапуск, щоб деякі зміни набрали чинності.\",\n  \"system\": \"Система\",\n  \"tag\": \"Теги\",\n  \"tapToStartDiscovery\": \"Натисніть кнопку пошуку, щоб виявити SSH сервери у вашій мережі\",\n  \"termFontSizeTip\": \"Це налаштування вплине на розмір терміналу (ширину та висоту). Ви можете масштабувати на сторінці терміналу, щоб налаштувати розмір шрифту поточної сесії.\",\n  \"textScaler\": \"Масштабування тексту\",\n  \"textScalerTip\": \"1.0 => 100% (оригінальний розмір), працює лише на частині шрифта сторінки сервера, не рекомендується змінювати.\",\n  \"time\": \"Час\",\n  \"times\": \"Рази\",\n  \"trySudo\": \"Спробуйте використовувати sudo\",\n  \"unknown\": \"Невідомо\",\n  \"unkownConvertMode\": \"Невідомий режим конвертації\",\n  \"update\": \"Оновити\",\n  \"updateIntervalEqual0\": \"Ви встановили 0, автоматичне оновлення не відбудеться.\\nНе можна розрахувати статус ЦП.\",\n  \"updateServerStatusInterval\": \"Інтервал оновлення статусу сервера\",\n  \"upsideDown\": \"Доверху дном\",\n  \"useCdn\": \"Використання CDN\",\n  \"useCdnTip\": \"Нереспонсивним користувачам рекомендується використовувати CDN. Чи хочете ви його використовувати?\",\n  \"useNoPwd\": \"Пароль не буде використовуватися\",\n  \"usePodmanByDefault\": \"Використовувати Podman за замовчуванням\",\n  \"used\": \"Використано\",\n  \"view\": \"Переглянути\",\n  \"viewDetails\": \"Переглянути деталі\",\n  \"viewErr\": \"Переглянути помилку\",\n  \"virtKeyHelpClipboard\": \"Копіювати в буфер обміну, якщо вибраний термінал не порожній, в іншому випадку вставити вміст буфера обміну в термінал.\",\n  \"virtKeyHelpIME\": \"Увімкнути/вимкнути клавіатуру\",\n  \"virtKeyHelpSFTP\": \"Відкрити поточний каталог у SFTP.\",\n  \"waitConnection\": \"Будь ласка, зачекайте, доки з'єднання буде встановлено.\",\n  \"wakeLock\": \"Залишити активним\",\n  \"watchNotPaired\": \"Немає спарованого Apple Watch\",\n  \"webdavSettingEmpty\": \"Налаштування WebDav порожнє\",\n  \"whenOpenApp\": \"При відкритті програми\",\n  \"wolTip\": \"Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.\",\n  \"write\": \"Записати\",\n  \"writeScriptFailTip\": \"Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.\",\n  \"writeScriptTip\": \"Після підключення до сервера скрипт буде записано у `~/.config/server_box` \\n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.\",\n  \"podmanDockerEmulationDetected\": \"Виявлено емуляцію Podman Docker. Будь ласка, переключіться на Podman у налаштуваннях.\"\n}\n"
  },
  {
    "path": "lib/l10n/app_zh.arb",
    "content": "{\n  \"@@locale\": \"zh\",\n  \"aboutThanks\": \"感谢以下参与的各位。\",\n  \"acceptBeta\": \"接受测试版更新推送\",\n  \"addSystemPrivateKeyTip\": \"检测到暂无私钥，是否添加系统默认的私钥（~/.ssh/id_rsa）？\",\n  \"added2List\": \"已添加至任务列表\",\n  \"addr\": \"地址\",\n  \"alreadyLastDir\": \"已是顶级目录\",\n  \"askAi\": \"问 AI\",\n  \"askAiApiKey\": \"API 密钥\",\n  \"askAiAwaitingResponse\": \"等待 AI 响应...\",\n  \"askAiBaseUrl\": \"基础 URL\",\n  \"askAiCommandInserted\": \"命令已插入终端\",\n  \"askAiConfigMissing\": \"请前往设置配置 {fields}\",\n  \"askAiConfirmExecute\": \"执行前确认\",\n  \"askAiConversation\": \"AI 对话\",\n  \"askAiDisclaimer\": \"AI 可能会犯错，请谨慎使用。\",\n  \"askAiFollowUpHint\": \"继续提问...\",\n  \"askAiInsertTerminal\": \"插入终端\",\n  \"askAiNoResponse\": \"无回复内容\",\n  \"askAiRecommendedCommand\": \"AI 推荐命令\",\n  \"askAiSelectedContent\": \"选中的内容\",\n  \"askAiUsageHint\": \"用于 SSH 终端\",\n  \"atLeastOneTab\": \"至少需要选择一个标签\",\n  \"authFailTip\": \"认证失败，请检查连接信息是否正确\",\n  \"autoBackupConflict\": \"仅可启用一个自动备份任务\",\n  \"autoConnect\": \"自动连接\",\n  \"autoRun\": \"自动运行\",\n  \"autoUpdateHomeWidget\": \"自动更新桌面小部件\",\n  \"availableTabs\": \"可用标签\",\n  \"backupEncrypted\": \"备份已加密\",\n  \"backupNotEncrypted\": \"备份未加密\",\n  \"backupPassword\": \"备份密码\",\n  \"backupPasswordRemoved\": \"备份密码已移除\",\n  \"backupPasswordSet\": \"备份密码已设置\",\n  \"backupPasswordTip\": \"设置密码以加密备份文件。留空则禁用加密。\",\n  \"backupPasswordWrong\": \"备份密码错误\",\n  \"backupTip\": \"导出数据可通过密码加密，请妥善保管。\",\n  \"backupVersionNotMatch\": \"备份版本不兼容，无法恢复\",\n  \"bgRun\": \"后台运行\",\n  \"bgRunTip\": \"此开关只代表程序会尝试在后台运行，具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”，MIUI / HyperOS 请将省电策略改为“无限制”。\",\n  \"clearAllStatsContent\": \"确定要清空所有服务器的连接统计数据吗？此操作无法撤销。\",\n  \"clearAllStatsTitle\": \"清空所有统计\",\n  \"clearServerStatsContent\": \"确定要清空服务器 \\\"{serverName}\\\" 的连接统计数据吗？此操作无法撤销。\",\n  \"clearServerStatsTitle\": \"清空 {serverName} 统计\",\n  \"clearThisServerStats\": \"清空此服务器统计\",\n  \"compactDatabase\": \"压缩数据库\",\n  \"compactDatabaseContent\": \"数据库大小：{size}\\n\\n此操作将重新组织数据库以减少体积，数据不会丢失。\",\n  \"closeAfterSave\": \"保存后关闭\",\n  \"collapseUITip\": \"是否默认折叠 UI 中的长列表\",\n  \"connectionDetails\": \"连接详情\",\n  \"connectionStats\": \"连接统计\",\n  \"connectionStatsDesc\": \"查看服务器连接成功率和历史记录\",\n  \"containerTrySudoTip\": \"例如：在应用内将用户设置为 aaa，但是 Docker 安装在root用户下，这时就需要启用此选项\",\n  \"containerSudoPasswordRequired\": \"需要 sudo 密码才能访问 Docker。请输入您的密码。\",\n  \"containerSudoPasswordIncorrect\": \"sudo 密码错误或无权限。请重试。\",\n  \"convert\": \"转换\",\n  \"copyPath\": \"复制路径\",\n  \"cpuViewAsProgressTip\": \"以进度条样式显示每个 CPU 的使用率（旧版样式）\",\n  \"cursorType\": \"光标类型\",\n  \"customCmd\": \"自定义命令\",\n  \"customCmdHint\": \"\\\"命令名称\\\": \\\"命令\\\"\",\n  \"deleteServers\": \"批量删除服务器\",\n  \"desktopTerminalTip\": \"启动 SSH 连接所用的终端模拟器命令\",\n  \"dirEmpty\": \"请确保目录为空\",\n  \"discoverSshServers\": \"发现SSH服务器\",\n  \"discoveryFailed\": \"发现失败\",\n  \"discoverySettings\": \"发现设置\",\n  \"discoverySummary\": \"发现摘要\",\n  \"diskHealth\": \"磁盘健康\",\n  \"diskIgnorePath\": \"忽略的磁盘路径\",\n  \"displayCpuIndex\": \"显示 CPU 索引\",\n  \"dl2Local\": \"下载 {fileName} 到本地？\",\n  \"dockerEmptyRunningItems\": \"没有正在运行的容器。\\n这可能是因为：\\n- Docker 安装用户与 App 内配置的用户名不同\\n- 环境变量 DOCKER_HOST 没有被正确读取。可以通过在终端内运行 `echo $DOCKER_HOST` 来获取。\",\n  \"dockerImagesFmt\": \"{count} 个镜像\",\n  \"dockerNotInstalled\": \"未安装 Docker\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} 个正在运行, {stoppedCount} 个已停止\",\n  \"dockerStatusRunningFmt\": \"{count} 个容器正在运行\",\n  \"doubleColumnMode\": \"双列模式\",\n  \"doubleColumnTip\": \"此选项仅用于启用该功能，是否生效取决于设备宽度\",\n  \"editVirtKeys\": \"编辑虚拟按键\",\n  \"editorHighlightTip\": \"代码高亮功能可能影响性能，可选择关闭。\",\n  \"enableMdns\": \"启用mDNS\",\n  \"enableMdnsDesc\": \"使用mDNS/Bonjour发现SSH服务\",\n  \"envVars\": \"环境变量\",\n  \"experimentalFeature\": \"实验性功能\",\n  \"extraArgs\": \"额外参数\",\n  \"fallbackSshDest\": \"备选 SSH 目标\",\n  \"fdroidReleaseTip\": \"如果你是从 F-Droid 下载的本应用，推荐关闭此选项\",\n  \"fgService\": \"前台服务\",\n  \"fgServiceTip\": \"开启后，可能会导致部分机型闪退。关闭可能导致部分机型无法后台保持 SSH 连接。请在系统设置内允许 ServerBox 通知权限、后台运行、自我唤醒。\",\n  \"fileTooLarge\": \"文件 '{file}' 过大 '{size}'，超过了 {sizeMax}\",\n  \"finishedAt\": \"完成于\",\n  \"followSystem\": \"跟随系统\",\n  \"fontSize\": \"字体大小\",\n  \"fullScreen\": \"全屏模式\",\n  \"fullScreenJitter\": \"全屏模式抖动\",\n  \"fullScreenJitterHelp\": \"用于防止屏幕烧屏\",\n  \"fullScreenTip\": \"当设备旋转为横屏时，是否开启全屏模式。此选项仅作用于服务器 Tab 页。\",\n  \"goBackQ\": \"返回？\",\n  \"goto\": \"前往\",\n  \"hideTitleBar\": \"隐藏标题栏\",\n  \"highlight\": \"代码高亮\",\n  \"homeTabs\": \"主页标签\",\n  \"homeTabsCustomizeDesc\": \"自定义主页上显示的标签及其顺序\",\n  \"homeWidgetUrlConfig\": \"桌面部件链接配置\",\n  \"httpFailedWithCode\": \"请求失败，状态码: {code}\",\n  \"ignoreCert\": \"忽略证书\",\n  \"image\": \"镜像\",\n  \"imagesList\": \"镜像列表\",\n  \"installDockerWithUrl\": \"请先前往 https://docs.docker.com/engine/install 安装 Docker\",\n  \"invalid\": \"无效\",\n  \"invalidHostFormat\": \"主机格式无效，仅支持 IPv4、IPv6 和域名字符。\",\n  \"jumpServer\": \"跳板服务器\",\n  \"keepForeground\": \"请将应用保持在前台运行\",\n  \"keepStatusWhenErr\": \"保留上次的服务器状态\",\n  \"keepStatusWhenErrTip\": \"仅限于执行脚本出错\",\n  \"keyAuth\": \"密钥认证\",\n  \"lastFailure\": \"最后失败\",\n  \"lastSuccess\": \"最后成功\",\n  \"letterCache\": \"输入法字符缓存\",\n  \"letterCacheTip\": \"推荐关闭，但是关闭后无法输入 CJK 等文字\",\n  \"madeWithLove\": \"用❤️制作 by {myGithub}\",\n  \"max\": \"最大\",\n  \"maxConcurrency\": \"最大并发数\",\n  \"maxRetryCount\": \"服务器尝试重连次数\",\n  \"maxRetryCountEqual0\": \"将无限次重试\",\n  \"min\": \"最小\",\n  \"more\": \"更多\",\n  \"moveOutServerFuncBtnsHelp\": \"开启：可以在服务器 Tab 页的每个卡片下方显示。关闭：在服务器详情页顶部显示。\",\n  \"needHomeDir\": \"如果你是群晖用户，[看这里](https://kb.synology.cn/zh-cn/DSM/tutorial/ssh_could_not_chdir_to_home_directory)。其他系统用户，需搜索如何创建家目录（home directory）.\",\n  \"needRestart\": \"需要重启 App\",\n  \"netViewType\": \"网络视图类型\",\n  \"newContainer\": \"新建容器\",\n  \"noConnectionStatsData\": \"暂无连接统计数据\",\n  \"noLineChart\": \"不使用折线图\",\n  \"noLineChartForCpu\": \"CPU 不使用折线图\",\n  \"noPrivateKeyTip\": \"私钥不存在，可能已被删除/配置错误\",\n  \"noPromptAgain\": \"不再提示\",\n  \"onServerDetailPage\": \"在服务器详情页\",\n  \"onlyOneLine\": \"仅显示为一行（可滚动）\",\n  \"onlyWhenCoreBiggerThan8\": \"仅当核心数大于 8 时生效\",\n  \"openLastPath\": \"打开上次的路径\",\n  \"openLastPathTip\": \"将为每台服务器记录其最后访问路径\",\n  \"parseContainerStatsTip\": \"Docker 解析占用状态较为缓慢\",\n  \"percentOfSize\": \"{size} 的 {percent}%\",\n  \"permission\": \"权限\",\n  \"pingInputIP\": \"请输入目标IP或域名\",\n  \"pingNoServer\": \"没有服务器可用于 Ping\\n请在服务器 tab 添加服务器后再试\",\n  \"plugInType\": \"插入类型\",\n  \"preferDiskAmount\": \"优先显示硬盘容量\",\n  \"privateKey\": \"私钥\",\n  \"privateKeyNotFoundFmt\": \"未找到私钥 [{keyId}]。\",\n  \"pushToken\": \"消息推送 Token\",\n  \"pveIgnoreCertTip\": \"不推荐开启，注意安全隐患！如果你使用的 PVE 默认证书，需要开启该选项\",\n  \"pveLoginFailed\": \"登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。\",\n  \"pveVersionLow\": \"当前该功能处于测试阶段，仅在 PVE 8+ 上测试过，请谨慎使用\",\n  \"read\": \"读\",\n  \"recentConnections\": \"最近连接记录\",\n  \"rememberPwdInMem\": \"在内存中记住密码\",\n  \"rememberPwdInMemTip\": \"用于容器、挂起等\",\n  \"rememberWindowSize\": \"记住窗口大小\",\n  \"remotePath\": \"远端路径\",\n  \"result\": \"结果\",\n  \"rotateAngel\": \"旋转角度\",\n  \"sameIdServerExist\": \"已存在相同 id 的服务器\",\n  \"save\": \"保存\",\n  \"second\": \"秒\",\n  \"serverDetailOrder\": \"详情页部件顺序\",\n  \"serverFuncBtns\": \"服务器功能按钮\",\n  \"serverOrder\": \"服务器顺序\",\n  \"serverTabRequired\": \"服务器标签不能被移除\",\n  \"sftpDlPrepare\": \"准备连接至服务器...\",\n  \"sftpEditorTip\": \"如果为空, 使用App内置的文件编辑器. 如果有值, 这是用远程服务器的编辑器, 例如 `vim` (建议根据 `EDITOR` 自动获取).\",\n  \"sftpRmrDirSummary\": \"在 SFTP 中使用 `rm -r` 来删除文件夹\",\n  \"sftpSSHConnected\": \"SFTP 已连接\",\n  \"sftpShowFoldersFirst\": \"文件夹显示在前\",\n  \"showDistLogo\": \"显示发行版 Logo\",\n  \"size\": \"大小\",\n  \"softWrap\": \"自动换行\",\n  \"specifyDev\": \"指定设备\",\n  \"specifyDevTip\": \"例如网络流量统计默认是所有设备，你可以在这里指定特定的设备\",\n  \"speed\": \"速度\",\n  \"spentTime\": \"耗时：{time}\",\n  \"sshConfigAllExist\": \"所有服务器已存在（发现 {duplicateCount} 个重复项）\",\n  \"sshConfigDuplicatesSkipped\": \"{duplicateCount} 个重复项将被跳过\",\n  \"sshConfigFound\": \"我们在您的系统中发现了 SSH 配置。\",\n  \"sshConfigFoundServers\": \"发现 {totalCount} 个服务器\",\n  \"sshConfigImport\": \"SSH 配置导入\",\n  \"sshConfigImportHelp\": \"只能导入基础信息，例如：IP/端口\",\n  \"sshConfigImportPermission\": \"是否允许读取 ~/.ssh/config 并自动导入服务器设置？\",\n  \"sshConfigImportTip\": \"首次创建服务器时提示读取 ~/.ssh/config\",\n  \"sshConfigImported\": \"从 SSH 配置导入了 {count} 个服务器\",\n  \"sshHostKeyChangedDesc\": \"服务器 {serverName} 的 SSH 主机密钥已更改，仅在信任该服务器时继续。\",\n  \"sshHostKeyFingerprintMd5Base64\": \"指纹（MD5 Base64）：{fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"指纹（MD5 十六进制）：{fingerprint}\",\n  \"sshHostKeyType\": \"SSH 主机密钥类型\",\n  \"sshHostKeyNewDesc\": \"收到来自 {serverName} 的新 SSH 主机密钥，在信任前请检查指纹。\",\n  \"sshHostKeyStoredFingerprint\": \"已存储的指纹：{fingerprint}\",\n  \"sshConfigManualSelect\": \"是否要手动选择 SSH 配置文件？\",\n  \"sshConfigNoServers\": \"SSH 配置中未找到服务器\",\n  \"sshConfigPermissionDenied\": \"由于 macOS 权限限制，无法访问 SSH 配置文件。\",\n  \"sshConfigServersToImport\": \"{importCount} 个服务器将被导入\",\n  \"sshTermHelp\": \"在终端可滚动时，横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容，在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。\",\n  \"sshTip\": \"该功能目前处于测试阶段。\\n\\n请在 {url} 反馈问题，或者加入我们开发。\",\n  \"sshVirtualKeyAutoOff\": \"虚拟按键自动切换\",\n  \"stat\": \"统计\",\n  \"supportFmtArgs\": \"支持以下格式化参数：\",\n  \"suspendTip\": \"suspend 功能需要 root 权限及 systemd 支持。\",\n  \"switchTo\": \"切换到 {val}\",\n  \"syncTip\": \"可能需要重新启动，某些更改才能生效。\",\n  \"system\": \"系统\",\n  \"tag\": \"标签\",\n  \"tapToStartDiscovery\": \"点击搜索按钮发现网络中的SSH服务器\",\n  \"termFontSizeTip\": \"此设置会影响终端大小（宽和高）。可以在终端页面缩放来调整当前会话的字体大小\",\n  \"textScaler\": \"字体缩放\",\n  \"textScalerTip\": \"1.0 => 100%（原大小），仅作用于服务器页面部分字体，不建议修改。\",\n  \"time\": \"时间\",\n  \"times\": \"次\",\n  \"trySudo\": \"尝试使用 sudo\",\n  \"unknown\": \"未知\",\n  \"unkownConvertMode\": \"未知转换模式\",\n  \"update\": \"更新\",\n  \"updateIntervalEqual0\": \"设置为 0 将不自动刷新服务器状态。\\n且无法计算 CPU 使用率。\",\n  \"updateServerStatusInterval\": \"服务器状态刷新间隔\",\n  \"upsideDown\": \"上下交换\",\n  \"useCdn\": \"使用 CDN\",\n  \"useCdnTip\": \"非中国大陆用户推荐使用 CDN，是否使用？\",\n  \"useNoPwd\": \"将会使用无密码\",\n  \"usePodmanByDefault\": \"默认使用 Podman\",\n  \"used\": \"已用\",\n  \"view\": \"视图\",\n  \"viewDetails\": \"查看详情\",\n  \"viewErr\": \"查看错误\",\n  \"virtKeyHelpClipboard\": \"如果终端有选中字符，则复制选中字符至剪切板，否则粘贴剪切板内容至终端。\",\n  \"virtKeyHelpIME\": \"打开/关闭键盘\",\n  \"virtKeyHelpSFTP\": \"在 SFTP 中打开当前路径。\",\n  \"waitConnection\": \"请等待连接建立\",\n  \"wakeLock\": \"保持唤醒\",\n  \"watchNotPaired\": \"没有已配对的 Apple Watch\",\n  \"webdavSettingEmpty\": \"WebDav 设置项为空\",\n  \"whenOpenApp\": \"当打开 App 时\",\n  \"wolTip\": \"配置 WOL 后，每次连接服务器时将自动发送唤醒请求\",\n  \"write\": \"写\",\n  \"writeScriptFailTip\": \"写入脚本失败，可能是没有权限/目录不存在等\",\n  \"writeScriptTip\": \"在连接服务器后，会向 `~/.config/server_box` \\n | `/tmp/server_box` 写入脚本来监测系统状态，你可以审查脚本内容。\",\n  \"menuGitHubRepository\": \"GitHub 仓库\",\n  \"podmanDockerEmulationDetected\": \"检测到 Podman Docker 仿真。请在设置中切换到 Podman。\"\n}\n"
  },
  {
    "path": "lib/l10n/app_zh_tw.arb",
    "content": "{\n  \"@@locale\": \"zh_TW\",\n  \"aboutThanks\": \"感謝以下參與的各位。\",\n  \"acceptBeta\": \"接受測試版更新推送\",\n  \"addSystemPrivateKeyTip\": \"偵測到尚無私鑰，是否要加入系統預設的私鑰（~/.ssh/id_rsa）？\",\n  \"added2List\": \"已新增至任務清單\",\n  \"addr\": \"位址\",\n  \"alreadyLastDir\": \"已是頂層目錄\",\n  \"askAi\": \"詢問 AI\",\n  \"askAiApiKey\": \"API 金鑰\",\n  \"askAiAwaitingResponse\": \"等待 AI 回應...\",\n  \"askAiBaseUrl\": \"基礎 URL\",\n  \"askAiCommandInserted\": \"指令已插入終端機\",\n  \"askAiConfigMissing\": \"請前往設定配置 {fields}\",\n  \"askAiConfirmExecute\": \"執行前確認\",\n  \"askAiConversation\": \"AI 對話\",\n  \"askAiDisclaimer\": \"AI 可能會犯錯，請謹慎使用。\",\n  \"askAiFollowUpHint\": \"繼續提問...\",\n  \"askAiInsertTerminal\": \"插入終端機\",\n  \"askAiNoResponse\": \"無回覆內容\",\n  \"askAiRecommendedCommand\": \"AI 推薦指令\",\n  \"askAiSelectedContent\": \"選取的內容\",\n  \"askAiUsageHint\": \"於 SSH 終端機中使用\",\n  \"atLeastOneTab\": \"至少需要選擇一個標籤\",\n  \"authFailTip\": \"認證失敗，請檢查連線資訊是否正確\",\n  \"autoBackupConflict\": \"僅能啟用一項自動備份任務\",\n  \"autoConnect\": \"自動連線\",\n  \"autoRun\": \"自動執行\",\n  \"autoUpdateHomeWidget\": \"自動更新桌面小工具\",\n  \"availableTabs\": \"可用標籤\",\n  \"backupEncrypted\": \"備份已加密\",\n  \"backupNotEncrypted\": \"備份未加密\",\n  \"backupPassword\": \"備份密碼\",\n  \"backupPasswordRemoved\": \"備份密碼已移除\",\n  \"backupPasswordSet\": \"備份密碼已設定\",\n  \"backupPasswordTip\": \"設定密碼來加密備份檔案。留空則停用加密。\",\n  \"backupPasswordWrong\": \"備份密碼錯誤\",\n  \"backupTip\": \"匯出的資料可透過密碼加密，請妥善保管。\",\n  \"backupVersionNotMatch\": \"備份版本不相容，無法還原\",\n  \"bgRun\": \"背景執行\",\n  \"bgRunTip\": \"此開關僅代表程式會嘗試於背景執行，能否成功取決於系統權限。在原生 Android 上，請關閉本應用的「電池最佳化」；在 MIUI / HyperOS 上，請將省電策略調整為「無限制」。\",\n  \"clearAllStatsContent\": \"確定要清空所有伺服器的連線統計資料嗎？此操作無法撤銷。\",\n  \"clearAllStatsTitle\": \"清空所有統計\",\n  \"clearServerStatsContent\": \"確定要清空伺服器 \\\"{serverName}\\\" 的連線統計資料嗎？此操作無法撤銷。\",\n  \"clearServerStatsTitle\": \"清空 {serverName} 統計\",\n  \"clearThisServerStats\": \"清空此伺服器統計\",\n  \"compactDatabase\": \"壓縮資料庫\",\n  \"compactDatabaseContent\": \"資料庫大小：{size}\\n\\n此操作將重新組織資料庫以減少體積，資料不會遺失。\",\n  \"closeAfterSave\": \"儲存後關閉\",\n  \"collapseUITip\": \"是否預設折疊 UI 中存在的長列表\",\n  \"connectionDetails\": \"連線詳情\",\n  \"connectionStats\": \"連線統計\",\n  \"connectionStatsDesc\": \"檢視伺服器連線成功率和歷史記錄\",\n  \"containerTrySudoTip\": \"例如：App 內設定使用者為 aaa，但是 Docker 安裝在 root 使用者，這時就需要開啟此選項\",\n  \"containerSudoPasswordRequired\": \"需要 sudo 密碼才能存取 Docker。請輸入您的密碼。\",\n  \"containerSudoPasswordIncorrect\": \"sudo 密碼錯誤或無權限。請重試。\",\n  \"convert\": \"轉換\",\n  \"copyPath\": \"複製路徑\",\n  \"cpuViewAsProgressTip\": \"以進度條樣式顯示每個CPU的使用率（舊版樣式）\",\n  \"cursorType\": \"游標類型\",\n  \"customCmd\": \"自訂指令\",\n  \"customCmdHint\": \"\\\"指令名稱\\\": \\\"指令\\\"\",\n  \"deleteServers\": \"大量刪除伺服器\",\n  \"desktopTerminalTip\": \"啟動 SSH 連線時用於打開終端機模擬器的指令。\",\n  \"dirEmpty\": \"請確保目錄為空\",\n  \"discoverSshServers\": \"發現SSH服務器\",\n  \"discoveryFailed\": \"發現失敗\",\n  \"discoverySettings\": \"發現設定\",\n  \"discoverySummary\": \"發現摘要\",\n  \"diskHealth\": \"磁碟健康\",\n  \"diskIgnorePath\": \"忽略的磁碟路徑\",\n  \"displayCpuIndex\": \"顯示 CPU 索引\",\n  \"dl2Local\": \"下載 {fileName} 到本地？\",\n  \"dockerEmptyRunningItems\": \"沒有正在執行的容器。\\n這可能是因為：\\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\\n- 環境變數 DOCKER_HOST 沒有被正確讀取。你可以通過在終端機內執行 `echo $DOCKER_HOST` 來獲取。\",\n  \"dockerImagesFmt\": \"{count} 個映像檔\",\n  \"dockerNotInstalled\": \"未安裝 Docker\",\n  \"dockerStatusRunningAndStoppedFmt\": \"{runningCount} 個正在執行, {stoppedCount} 個已停止\",\n  \"dockerStatusRunningFmt\": \"{count} 個容器正在執行\",\n  \"doubleColumnMode\": \"雙列模式\",\n  \"doubleColumnTip\": \"此選項僅用於啟用此功能，是否生效取決於裝置寬度\",\n  \"editVirtKeys\": \"編輯虛擬按鍵\",\n  \"editorHighlightTip\": \"程式碼高亮功能可能影響效能，可選擇性關閉。\",\n  \"enableMdns\": \"啟用mDNS\",\n  \"enableMdnsDesc\": \"使用mDNS/Bonjour發現SSH服務\",\n  \"envVars\": \"環境變數\",\n  \"experimentalFeature\": \"實驗性功能\",\n  \"extraArgs\": \"額外參數\",\n  \"fallbackSshDest\": \"備選 SSH 目標\",\n  \"fdroidReleaseTip\": \"如果你是從 F-Droid 下載的本App，推薦關閉此選項\",\n  \"fgService\": \"前台服務\",\n  \"fgServiceTip\": \"開啟後，可能會導致部分機型閃退。關閉可能導致部分機型無法背景保持 SSH 連線。請在系統設定內允許 ServerBox 通知權限、背景執行、自我喚醒。\",\n  \"fileTooLarge\": \"檔案 '{file}' 過大 '{size}'，超過了 {sizeMax}\",\n  \"finishedAt\": \"完成於\",\n  \"followSystem\": \"跟隨系統\",\n  \"fontSize\": \"字型大小\",\n  \"fullScreen\": \"全螢幕模式\",\n  \"fullScreenJitter\": \"全螢幕模式抖動\",\n  \"fullScreenJitterHelp\": \"防止螢幕烙印\",\n  \"fullScreenTip\": \"當設備旋轉為橫向時，是否開啟全螢幕模式？此選項僅適用於伺服器分頁。\",\n  \"goBackQ\": \"返回？\",\n  \"goto\": \"前往\",\n  \"hideTitleBar\": \"隱藏標題欄\",\n  \"highlight\": \"程式碼標記\",\n  \"homeTabs\": \"主頁標籤\",\n  \"homeTabsCustomizeDesc\": \"自訂主頁上顯示的標籤及其順序\",\n  \"homeWidgetUrlConfig\": \"桌面小工具連結配置\",\n  \"httpFailedWithCode\": \"請求失敗，狀態碼：{code}\",\n  \"ignoreCert\": \"忽略憑證\",\n  \"image\": \"映像檔\",\n  \"imagesList\": \"映像檔列表\",\n  \"installDockerWithUrl\": \"請先前往 https://docs.docker.com/engine/install 安裝 Docker\",\n  \"invalid\": \"無效\",\n  \"invalidHostFormat\": \"主機格式無效，僅支援 IPv4、IPv6 和網域字元。\",\n  \"jumpServer\": \"跳板伺服器\",\n  \"keepForeground\": \"請讓 App 保持在前景執行\",\n  \"keepStatusWhenErr\": \"保留上次的伺服器狀態\",\n  \"keepStatusWhenErrTip\": \"僅在執行腳本出錯時\",\n  \"keyAuth\": \"金鑰認證\",\n  \"lastFailure\": \"最後失敗\",\n  \"lastSuccess\": \"最後成功\",\n  \"letterCache\": \"輸入法字符快取\",\n  \"letterCacheTip\": \"建議關閉，但關閉後將無法輸入 CJK 等文字。\",\n  \"madeWithLove\": \"用❤️製作 by {myGithub}\",\n  \"max\": \"最大\",\n  \"maxConcurrency\": \"最大並發數\",\n  \"maxRetryCount\": \"伺服器嘗試重連次數\",\n  \"maxRetryCountEqual0\": \"將無限次重試\",\n  \"min\": \"最小\",\n  \"more\": \"更多\",\n  \"moveOutServerFuncBtnsHelp\": \"開啟：可以在伺服器 Tab 頁的每個卡片下方顯示。關閉：在伺服器詳情頁頂部顯示。\",\n  \"needHomeDir\": \"如果你是群暉使用者，[看這裡](https://kb.synology.com/DSM/tutorial/user_enable_home_service)。其他系統使用者，需搜尋如何建立家目錄（home directory）。\",\n  \"needRestart\": \"需要重開 App\",\n  \"netViewType\": \"網路檢視類型\",\n  \"newContainer\": \"新建容器\",\n  \"noConnectionStatsData\": \"暫無連線統計資料\",\n  \"noLineChart\": \"不使用折線圖\",\n  \"noLineChartForCpu\": \"CPU 不使用折線圖\",\n  \"noPrivateKeyTip\": \"私鑰不存在，可能已被刪除/配置錯誤。\",\n  \"noPromptAgain\": \"不再提示\",\n  \"onServerDetailPage\": \"在伺服器詳情頁\",\n  \"onlyOneLine\": \"僅顯示為一行（可捲動）\",\n  \"onlyWhenCoreBiggerThan8\": \"僅當核心數大於 8 時生效\",\n  \"openLastPath\": \"打開上次的路徑\",\n  \"openLastPathTip\": \"將為每台伺服器紀錄其最後存取路徑\",\n  \"parseContainerStatsTip\": \"Docker 解析消耗狀態較為緩慢\",\n  \"percentOfSize\": \"{size} 的 {percent}%\",\n  \"permission\": \"權限\",\n  \"pingInputIP\": \"請輸入目標 IP 或域名\",\n  \"pingNoServer\": \"沒有伺服器可用於 Ping\\n請在伺服器 Tab 新增伺服器後再試\",\n  \"plugInType\": \"插入類型\",\n  \"preferDiskAmount\": \"優先顯示硬碟容量\",\n  \"privateKey\": \"私鑰\",\n  \"privateKeyNotFoundFmt\": \"未找到私鑰 [{keyId}]。\",\n  \"pushToken\": \"消息推送 Token\",\n  \"pveIgnoreCertTip\": \"不建議啟用，請注意安全風險！如果您使用的是 PVE 的預設憑證，則需要啟用此選項。\",\n  \"pveLoginFailed\": \"登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。\",\n  \"pveVersionLow\": \"此功能目前處於測試階段，僅在 PVE 8+ 上進行過測試。請謹慎使用。\",\n  \"read\": \"讀取\",\n  \"recentConnections\": \"最近連線記錄\",\n  \"rememberPwdInMem\": \"在記憶體中記住密碼\",\n  \"rememberPwdInMemTip\": \"用於容器、暫停等\",\n  \"rememberWindowSize\": \"記住視窗大小\",\n  \"remotePath\": \"遠端路徑\",\n  \"result\": \"結果\",\n  \"rotateAngel\": \"旋轉角度\",\n  \"sameIdServerExist\": \"已存在相同 ID 的伺服器\",\n  \"save\": \"儲存\",\n  \"second\": \"秒\",\n  \"serverDetailOrder\": \"詳情頁部件順序\",\n  \"serverFuncBtns\": \"伺服器功能按鈕\",\n  \"serverOrder\": \"伺服器順序\",\n  \"serverTabRequired\": \"服務器標籤不能被移除\",\n  \"sftpDlPrepare\": \"準備連線至伺服器...\",\n  \"sftpEditorTip\": \"如果為空, 使用App內建的檔案編輯器。如果有值, 則使用遠端伺服器的編輯器, 例如 `vim`（建議根據 `EDITOR` 自動獲取）。\",\n  \"sftpRmrDirSummary\": \"在 SFTP 中使用 `rm -r` 來刪除檔案夾\",\n  \"sftpSSHConnected\": \"SFTP 已連線\",\n  \"sftpShowFoldersFirst\": \"資料夾顯示在前\",\n  \"showDistLogo\": \"顯示發行版 Logo\",\n  \"size\": \"大小\",\n  \"softWrap\": \"軟換行\",\n  \"specifyDev\": \"指定裝置\",\n  \"specifyDevTip\": \"例如網路流量統計預設是所有裝置，你可以在這裡指定特定的裝置。\",\n  \"speed\": \"速度\",\n  \"spentTime\": \"耗時：{time}\",\n  \"sshConfigAllExist\": \"所有伺服器均已存在（發現{duplicateCount}個重複項）\",\n  \"sshConfigDuplicatesSkipped\": \"將跳過{duplicateCount}個重複項\",\n  \"sshConfigFound\": \"我們在您的系統中發現了SSH設定\",\n  \"sshConfigFoundServers\": \"發現{totalCount}個伺服器\",\n  \"sshConfigImport\": \"匯入SSH設定\",\n  \"sshConfigImportHelp\": \"只能匯入基礎資訊，例如：IP/端口。\",\n  \"sshConfigImportPermission\": \"您是否希望允許讀取 ~/.ssh/config 並自動匯入伺服器設定？\",\n  \"sshConfigImportTip\": \"在建立第一個伺服器時提示讀取 ~/.ssh/config\",\n  \"sshConfigImported\": \"已從SSH設定匯入{count}個伺服器\",\n  \"sshHostKeyChangedDesc\": \"伺服器 {serverName} 的 SSH 主機金鑰已變更，僅在信任該伺服器時繼續。\",\n  \"sshHostKeyFingerprintMd5Base64\": \"指紋（MD5 Base64）：{fingerprint}\",\n  \"sshHostKeyFingerprintMd5Hex\": \"指紋（MD5 十六進位）：{fingerprint}\",\n  \"sshHostKeyType\": \"SSH 主機金鑰類型\",\n  \"sshHostKeyNewDesc\": \"收到來自 {serverName} 的新 SSH 主機金鑰，信任前請先檢查指紋。\",\n  \"sshHostKeyStoredFingerprint\": \"已儲存的指紋：{fingerprint}\",\n  \"sshConfigManualSelect\": \"是否要手動選擇 SSH 設定檔案？\",\n  \"sshConfigNoServers\": \"SSH設定中未找到伺服器\",\n  \"sshConfigPermissionDenied\": \"由於 macOS 權限限制，無法存取 SSH 設定檔案。\",\n  \"sshConfigServersToImport\": \"將匯入{importCount}個伺服器\",\n  \"sshTermHelp\": \"在終端機可捲動時，橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容，在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。\",\n  \"sshTip\": \"該功能目前處於測試階段。\\n\\n請在 {url} 回饋問題，或者加入我們開發。\",\n  \"sshVirtualKeyAutoOff\": \"虛擬按鍵自動切換\",\n  \"stat\": \"統計\",\n  \"supportFmtArgs\": \"支援以下格式化參數：\",\n  \"suspendTip\": \"suspend 功能需要 root 權限及 systemd 支援。\",\n  \"switchTo\": \"切換到 {val}\",\n  \"syncTip\": \"可能需要重新啟動，某些更改才能生效。\",\n  \"system\": \"系統\",\n  \"tag\": \"標籤\",\n  \"tapToStartDiscovery\": \"點擊搜尋按鈕發現網路中的SSH服務器\",\n  \"termFontSizeTip\": \"此設定將影響終端機大小（寬度和高度）。您可以在終端機頁面縮放，來調整目前會話的字型大小。\",\n  \"textScaler\": \"字型縮放\",\n  \"textScalerTip\": \"1.0 => 100%（原大小），僅作用於伺服器頁面部分字型，不建議修改。\",\n  \"time\": \"時間\",\n  \"times\": \"次\",\n  \"trySudo\": \"嘗試使用 sudo\",\n  \"unknown\": \"未知\",\n  \"unkownConvertMode\": \"未知轉換模式\",\n  \"update\": \"更新\",\n  \"updateIntervalEqual0\": \"設定為 0 將不自動刷新伺服器狀態，\\n也無法計算 CPU 使用率。\",\n  \"updateServerStatusInterval\": \"伺服器狀態更新間隔\",\n  \"upsideDown\": \"上下交換\",\n  \"useCdn\": \"使用 CDN\",\n  \"useCdnTip\": \"非中國使用者建議使用 CDN，是否使用？\",\n  \"useNoPwd\": \"將使用無密碼\",\n  \"usePodmanByDefault\": \"預設使用 Podman\",\n  \"used\": \"已使用\",\n  \"view\": \"檢視\",\n  \"viewDetails\": \"檢視詳情\",\n  \"viewErr\": \"查看錯誤\",\n  \"virtKeyHelpClipboard\": \"如果終端機有選中字元，則復製選中字元至剪貼簿，否則貼上剪貼簿內容至終端機。\",\n  \"virtKeyHelpIME\": \"打開/關閉鍵盤\",\n  \"virtKeyHelpSFTP\": \"在 SFTP 中打開目前路徑。\",\n  \"waitConnection\": \"請等待連線建立\",\n  \"wakeLock\": \"保持喚醒\",\n  \"watchNotPaired\": \"沒有已配對的 Apple Watch\",\n  \"webdavSettingEmpty\": \"WebDav 設定項爲空\",\n  \"whenOpenApp\": \"當打開 App 時\",\n  \"wolTip\": \"設定 WOL 後，每次連線伺服器時將自動發送喚醒請求\",\n  \"write\": \"寫入\",\n  \"writeScriptFailTip\": \"寫入腳本失敗，可能是沒有權限/目錄不存在等。\",\n  \"writeScriptTip\": \"連線到伺服器後，將會在 `~/.config/server_box` \\n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。\",\n  \"podmanDockerEmulationDetected\": \"檢測到 Podman Docker 仿真。請在設定中切換到 Podman。\"\n}\n"
  },
  {
    "path": "lib/main.dart",
    "content": "// ignore_for_file: avoid_print\n\nimport 'dart:async';\n\nimport 'package:computer/computer.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_displaymode/flutter_displaymode.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:hive_ce_flutter/hive_flutter.dart';\nimport 'package:logging/logging.dart';\nimport 'package:server_box/app.dart';\nimport 'package:server_box/data/model/app/menu/server_func.dart';\nimport 'package:server_box/data/model/app/server_detail_card.dart';\nimport 'package:server_box/data/res/build_data.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/data/ssh/session_manager.dart';\nimport 'package:server_box/data/store/server.dart';\nimport 'package:server_box/hive/hive_registrar.g.dart';\n\nFuture<void> main() async {\n  _runInZone(() async {\n    await _initApp();\n    runApp(ProviderScope(child: const MyApp()));\n  });\n}\n\nvoid _runInZone(void Function() body) {\n  final zoneSpec = ZoneSpecification(\n    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {\n      parent.print(zone, line);\n    },\n  );\n\n  runZonedGuarded(body, (e, s) => print('[ZONE] $e\\n$s'), zoneSpecification: zoneSpec);\n}\n\nFuture<void> _initApp() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  await _initData();\n  _setupDebug();\n  await _initWindow();\n\n  _doPlatformRelated();\n\n  // Initialize Android session notification channel/handler\n  TermSessionManager.init();\n}\n\nFuture<void> _initData() async {\n  await Paths.init(BuildData.name, bakName: 'srvbox_bak.json');\n\n  await Hive.initFlutter();\n  Hive.registerAdapters();\n\n  await PrefStore.shared.init(); // Call this before accessing any store\n  await Stores.init();\n\n  // It may effect the following logic, so await it.\n  // DO DB migration before load any provider.\n  await _doDbMigrate();\n\n  if (Stores.setting.betaTest.fetch()) AppUpdate.chan = AppUpdateChan.beta;\n\n  FontUtils.loadFrom(Stores.setting.fontPath.fetch());\n}\n\nvoid _setupDebug() {\n  Logger.root.level = Level.ALL;\n  Logger.root.onRecord.listen((record) {\n    DebugProvider.addLog(record);\n    if (record.error != null) print(record.error);\n    if (record.stackTrace != null) print(record.stackTrace);\n  });\n}\n\nvoid _doPlatformRelated() async {\n  if (isAndroid) {\n    // try switch to highest refresh rate\n    FlutterDisplayMode.setHighRefreshRate();\n  }\n\n  final serversCount = Stores.server.keys().length;\n  Computer.shared.turnOn(workersCount: (serversCount / 3).round() + 1); // Plus 1 to avoid 0.\n}\n\n// It may contains some async heavy funcs.\nFuture<void> _doDbMigrate() async {\n  final lastVer = Stores.setting.lastVer.fetch();\n  const newVer = BuildData.build;\n  // It's only the version upgrade trigger logic.\n  // How to upgrade the data is inside each own func.\n  if (lastVer < newVer) {\n    ServerDetailCards.autoAddNewCards(newVer);\n    ServerFuncBtn.autoAddNewFuncs(newVer);\n    Stores.setting.lastVer.put(newVer);\n  }\n\n  // Migrate the old id to new id.\n  ServerStore.instance.migrateIds();\n}\n\nFuture<void> _initWindow() async {\n  if (!isDesktop) return;\n  final windowStateProp = Stores.setting.windowState;\n  final windowState = windowStateProp.fetch();\n  final hideTitleBar = Stores.setting.hideTitleBar.fetch();\n  WindowFrameConfig.setShowCaption(hideTitleBar);\n  await SystemUIs.initDesktopWindow(\n    hideTitleBar: hideTitleBar,\n    size: windowState?.size ?? Size(947, 487),\n    position: windowState?.position,\n    listener: WindowStateListener(windowStateProp),\n  );\n}\n"
  },
  {
    "path": "lib/view/page/backup.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:computer/computer.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/sync.dart';\nimport 'package:server_box/data/model/app/bak/backup2.dart';\nimport 'package:server_box/data/model/app/bak/backup_service.dart';\nimport 'package:server_box/data/model/app/bak/backup_source.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/snippet.dart';\nimport 'package:server_box/data/provider/snippet.dart';\nimport 'package:server_box/data/res/misc.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:webdav_client_plus/webdav_client_plus.dart';\n\nclass BackupPage extends ConsumerStatefulWidget {\n  const BackupPage({super.key});\n\n  @override\n  ConsumerState<BackupPage> createState() => _BackupPageState();\n\n  static const route = AppRouteNoArg(page: BackupPage.new, path: '/backup');\n}\n\nfinal class _BackupPageState extends ConsumerState<BackupPage> with AutomaticKeepAliveClientMixin {\n  final webdavLoading = false.vn;\n  final gistLoading = false.vn;\n\n  @override\n  void dispose() {\n    webdavLoading.dispose();\n    gistLoading.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return Scaffold(body: SafeArea(child: _buildBody));\n  }\n\n  Widget get _buildBody {\n    return MultiList(\n      widthDivider: 2,\n      children: [\n        [\n          CenterGreyTitle(libL10n.sync),\n          _buildTip,\n          _buildBakPwd,\n          if (isMacOS || isIOS) _buildIcloud,\n          _buildWebdav,\n          _buildGist,\n          _buildFile,\n          _buildClipboard,\n        ],\n        [CenterGreyTitle(libL10n.import), _buildBulkImportServers, _buildImportSnippet],\n      ],\n      );\n  }\n\n  Widget get _buildBakPwd {\n    return FutureBuilder<String?>(\n      future: SecureStoreProps.bakPwd.read(),\n      builder: (context, snapshot) {\n        final hasPwd = snapshot.data?.isNotEmpty == true;\n        return CardX(\n          child: ListTile(\n            leading: const Icon(Icons.lock),\n            title: Text(l10n.backupPassword),\n            subtitle: Text(hasPwd ? l10n.backupEncrypted : l10n.backupNotEncrypted, style: UIs.textGrey),\n            trailing: Row(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                TextButton(onPressed: () async => _onTapSetBakPwd(context), child: Text(libL10n.setting)),\n                if (hasPwd) ...[\n                  UIs.width7,\n                  TextButton(\n                    onPressed: () async {\n                      await SecureStoreProps.bakPwd.write(null);\n                      context.showSnackBar(l10n.backupPasswordRemoved);\n                      setState(() {});\n                    },\n                    child: Text(libL10n.delete),\n                  ),\n                ],\n              ],\n            ),\n            onTap: () async => _onTapSetBakPwd(context),\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _onTapSetBakPwd(BuildContext context) async {\n    final currentPwd = await SecureStoreProps.bakPwd.read();\n    final controller = TextEditingController(text: currentPwd ?? '');\n    final node = FocusNode();\n    final result = await context.showRoundDialog<bool>(\n      title: l10n.backupPassword,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Text(l10n.backupPasswordTip, style: UIs.textGrey),\n          UIs.height13,\n          Input(\n            label: l10n.backupPassword,\n            controller: controller,\n            node: node,\n            obscureText: true,\n            onSubmitted: (_) => context.pop(true),\n          ),\n        ],\n      ),\n      actions: Btnx.oks,\n    );\n    if (result == true) {\n      final pwd = controller.text.trim();\n      if (pwd.isEmpty) {\n        context.showSnackBar(libL10n.empty);\n        return;\n      }\n      await SecureStoreProps.bakPwd.write(pwd);\n      context.showSnackBar(l10n.backupPasswordSet);\n      setState(() {});\n    }\n  }\n\n  Widget get _buildTip {\n    return CardX(\n      child: ListTile(\n        leading: const Icon(Icons.warning),\n        title: Text(libL10n.attention),\n        subtitle: Text(l10n.backupTip, style: UIs.textGrey),\n      ),\n    );\n  }\n\n  Widget get _buildFile {\n    return CardX(\n      child: ExpandTile(\n        leading: const Icon(Icons.file_open),\n        title: Text(libL10n.file),\n        initiallyExpanded: false,\n        children: [\n          ListTile(\n            title: Text(libL10n.backup),\n            trailing: const Icon(Icons.save),\n            onTap: () => BackupService.backup(context, FileBackupSource()),\n          ),\n          ListTile(\n            trailing: const Icon(Icons.restore),\n            title: Text(libL10n.restore),\n            onTap: () => BackupService.restore(context, FileBackupSource()),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget get _buildIcloud {\n    return CardX(\n      child: ListTile(\n        leading: const Icon(Icons.cloud),\n        title: const Text('iCloud'),\n        trailing: StoreSwitch(\n          prop: PrefProps.icloudSync,\n          validator: (p0) async {\n            if (p0 && PrefProps.webdavSync.get()) {\n              context.showSnackBar(l10n.autoBackupConflict);\n              return false;\n            }\n            if (p0) {\n              final ok = await _ensureBakPwd(context);\n              if (!ok) return false;\n            }\n            if (p0) {\n              await bakSync.sync(rs: icloud);\n            }\n            return true;\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget get _buildWebdav {\n    return CardX(\n      child: ExpandTile(\n        leading: const Icon(Icons.storage),\n        title: const Text('WebDAV'),\n        initiallyExpanded: false,\n        children: [\n          ListTile(\n            title: Text(libL10n.setting),\n            trailing: const Icon(Icons.settings),\n            onTap: () async => _onTapWebdavSetting(context),\n          ),\n          ListTile(\n            title: Text(libL10n.auto),\n            trailing: StoreSwitch(\n              prop: PrefProps.webdavSync,\n              validator: (p0) async {\n                if (p0 && PrefProps.icloudSync.get()) {\n                  context.showSnackBar(l10n.autoBackupConflict);\n                  return false;\n                }\n                if (p0) {\n                  final ok = await _ensureBakPwd(context);\n                  if (!ok) return false;\n                }\n                if (p0) {\n                  final url = PrefProps.webdavUrl.get();\n                  final user = PrefProps.webdavUser.get();\n                  final pwd = PrefProps.webdavPwd.get();\n\n                  final anyNull = url == null || user == null || pwd == null;\n                  if (anyNull) {\n                    context.showSnackBar(l10n.webdavSettingEmpty);\n                    return false;\n                  }\n\n                  final anyEmpty = url.isEmpty || user.isEmpty || pwd.isEmpty;\n                  if (anyEmpty) {\n                    context.showSnackBar(l10n.webdavSettingEmpty);\n                    return false;\n                  }\n\n                  webdavLoading.value = true;\n                  await bakSync.sync(rs: Webdav.shared);\n                  webdavLoading.value = false;\n                }\n                return true;\n              },\n            ),\n          ),\n          ListTile(\n            title: Text(libL10n.manual),\n            trailing: webdavLoading.listenVal((loading) {\n              if (loading) return SizedLoading.small;\n\n              return Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  TextButton(onPressed: () async => _onTapWebdavDl(context), child: Text(libL10n.restore)),\n                  UIs.width7,\n                  TextButton(onPressed: () async => _onTapWebdavUp(context), child: Text(libL10n.backup)),\n                ],\n              );\n            }),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget get _buildGist {\n    return CardX(\n      child: ExpandTile(\n        leading: const Icon(Icons.code),\n        title: const Text('GitHub Gist'),\n        initiallyExpanded: false,\n        children: [\n          ListTile(\n            title: Text(libL10n.setting),\n            trailing: const Icon(Icons.settings),\n            onTap: () async => _onTapGistSetting(context),\n          ),\n          ListTile(\n            title: Text(libL10n.auto),\n            trailing: StoreSwitch(\n              prop: PrefProps.gistSync,\n              validator: (p0) async {\n                if (p0 && (PrefProps.icloudSync.get() || PrefProps.webdavSync.get())) {\n                  context.showSnackBar(l10n.autoBackupConflict);\n                  return false;\n                }\n                if (p0) {\n                  final ok = await _ensureBakPwd(context);\n                  if (!ok) return false;\n                }\n                if (p0) {\n                  final token = PrefProps.githubToken.get();\n                  // Allow empty gistId (will create one on first upload)\n                  final hasToken = token != null && token.isNotEmpty;\n                  if (!hasToken) {\n                    context.showSnackBar('Token or Gist ID is empty');\n                    return false;\n                  }\n                  gistLoading.value = true;\n                  await bakSync.sync(rs: GistRs.shared);\n                  gistLoading.value = false;\n                }\n                return true;\n              },\n            ),\n          ),\n          ListTile(\n            title: Text(libL10n.manual),\n            trailing: gistLoading.listenVal((loading) {\n              if (loading) return SizedLoading.small;\n\n              return Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  TextButton(onPressed: () async => _onTapGistDl(context), child: Text(libL10n.restore)),\n                  UIs.width7,\n                  TextButton(onPressed: () async => _onTapGistUp(context), child: Text(libL10n.backup)),\n                ],\n              );\n            }),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget get _buildClipboard {\n    return CardX(\n      child: ExpandTile(\n        leading: const Icon(Icons.content_paste),\n        title: Text(libL10n.clipboard),\n        children: [\n          ListTile(\n            title: Text(libL10n.backup),\n            trailing: const Icon(Icons.save),\n            onTap: () => BackupService.backup(context, ClipboardBackupSource()),\n          ),\n          ListTile(\n            trailing: const Icon(Icons.restore),\n            title: Text(libL10n.restore),\n            onTap: () => BackupService.restore(context, ClipboardBackupSource()),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget get _buildBulkImportServers {\n    return CardX(\n      child: ListTile(\n        title: Text(libL10n.server),\n        leading: const Icon(BoxIcons.bx_server),\n        onTap: () => _onBulkImportServers(context),\n        trailing: const Icon(Icons.keyboard_arrow_right),\n      ),\n    );\n  }\n\n  Widget get _buildImportSnippet {\n    return ListTile(\n      title: Text(libL10n.snippet),\n      leading: const Icon(MingCute.code_line),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () async {\n        final data = await context.showImportDialog(title: libL10n.snippet, modelDef: Snippet.example.toJson());\n        if (data == null) return;\n        String str;\n        try {\n          str = utf8.decode(data);\n        } on FormatException catch (e, s) {\n          context.showErrDialog(e, s, libL10n.error);\n          return;\n        }\n        final (list, _) = await context.showLoadingDialog(\n          fn: () => Computer.shared.start((s) {\n            return json.decode(s) as List;\n          }, str),\n        );\n        if (list == null || list.isEmpty) return;\n        final snippets = <Snippet>[];\n        final errs = <String>[];\n        for (final item in list) {\n          try {\n            final snippet = Snippet.fromJson(item);\n            snippets.add(snippet);\n          } catch (e) {\n            errs.add(e.toString());\n          }\n        }\n        if (snippets.isEmpty) {\n          context.showSnackBar(libL10n.empty);\n          return;\n        }\n        if (errs.isNotEmpty) {\n          context.showRoundDialog(\n            title: libL10n.error,\n            child: SingleChildScrollView(child: Text(errs.join('\\n'))),\n          );\n          return;\n        }\n        final snippetNames = snippets.map((e) => e.name).join(', ');\n        context.showRoundDialog(\n          title: libL10n.attention,\n          child: SingleChildScrollView(child: Text(libL10n.askContinue('${libL10n.import} [$snippetNames]'))),\n          actions: Btn.ok(\n            onTap: () {\n              final notifier = ref.read(snippetProvider.notifier);\n              for (final snippet in snippets) {\n                notifier.add(snippet);\n              }\n              context.pop();\n              context.pop();\n            },\n          ).toList,\n        );\n      },\n    ).cardx;\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n}\n\nextension on _BackupPageState {\n  Future<void> _onTapWebdavDl(BuildContext context) async {\n    webdavLoading.value = true;\n    try {\n      final files = await Webdav.shared.list();\n      if (files.isEmpty) return context.showSnackBar(l10n.dirEmpty);\n\n      final fileName = await context.showPickSingleDialog(title: libL10n.restore, items: files);\n      if (fileName == null) return;\n\n      await Webdav.shared.download(relativePath: fileName);\n      final dlFile = await File('${Paths.doc}/$fileName').readAsString();\n      await BackupService.restoreFromText(context, dlFile);\n    } catch (e, s) {\n      context.showErrDialog(e, s, libL10n.restore);\n      Loggers.app.warning('Download webdav backup failed', e, s);\n    } finally {\n      webdavLoading.value = false;\n    }\n  }\n\n  Future<void> _onTapWebdavUp(BuildContext context) async {\n    webdavLoading.value = true;\n    final date = DateTime.now().ymdhms(ymdSep: '-', hmsSep: '-', sep: '-');\n    final bakName = '$date-${Miscs.bakFileName}';\n    try {\n      final ok = await _ensureBakPwd(context);\n      if (!ok) return;\n      final savedPassword = await SecureStoreProps.bakPwd.read();\n      await BackupV2.backup(bakName, savedPassword?.isEmpty == true ? null : savedPassword);\n      await Webdav.shared.upload(relativePath: bakName);\n      Loggers.app.info('Upload webdav backup success');\n    } catch (e, s) {\n      context.showErrDialog(e, s, libL10n.upload);\n      Loggers.app.warning('Upload webdav backup failed', e, s);\n    } finally {\n      webdavLoading.value = false;\n    }\n  }\n\n  Future<void> _onTapGistDl(BuildContext context) async {\n    gistLoading.value = true;\n    try {\n      final files = await GistRs.shared.list();\n      if (files.isEmpty) return context.showSnackBar(l10n.dirEmpty);\n\n      final fileName = await context.showPickSingleDialog(title: libL10n.restore, items: files);\n      if (fileName == null) return;\n\n      await GistRs.shared.download(relativePath: fileName);\n      final dlFile = await File('${Paths.doc}/$fileName').readAsString();\n      await BackupService.restoreFromText(context, dlFile);\n    } catch (e, s) {\n      context.showErrDialog(e, s, libL10n.restore);\n      Loggers.app.warning('Download gist backup failed', e, s);\n    } finally {\n      gistLoading.value = false;\n    }\n  }\n\n  Future<void> _onTapGistUp(BuildContext context) async {\n    gistLoading.value = true;\n    final date = DateTime.now().ymdhms(ymdSep: '-', hmsSep: '-', sep: '-');\n    final bakName = '$date-${Miscs.bakFileName}';\n    try {\n      final ok = await _ensureBakPwd(context);\n      if (!ok) return;\n      final savedPassword = await SecureStoreProps.bakPwd.read();\n      await BackupV2.backup(bakName, savedPassword?.isEmpty == true ? null : savedPassword);\n      await GistRs.shared.upload(relativePath: bakName);\n      Loggers.app.info('Upload gist backup success');\n    } catch (e, s) {\n      context.showErrDialog(e, s, libL10n.upload);\n      Loggers.app.warning('Upload gist backup failed', e, s);\n    } finally {\n      gistLoading.value = false;\n    }\n  }\n\n  Future<void> _onTapGistSetting(BuildContext context) async {\n    final tokenCtrl = TextEditingController(text: PrefProps.githubToken.get());\n    final gistIdCtrl = TextEditingController(text: PrefProps.gistId.get());\n    final nodeToken = FocusNode();\n    final result = await context.showRoundDialog<bool>(\n      title: 'GitHub Gist',\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Input(label: 'Token', controller: tokenCtrl, suggestion: false, node: nodeToken),\n          Input(\n            label: 'Gist ID (optional)',\n            controller: gistIdCtrl,\n            suggestion: false,\n            onSubmitted: (_) => context.pop(true),\n          ),\n        ],\n      ),\n      actions: Btnx.oks,\n    );\n    if (result == true) {\n      try {\n        final token_ = tokenCtrl.text.trim();\n        final gistId_ = gistIdCtrl.text.trim();\n\n        await GistRs.test(token: token_, gistId: gistId_.isEmpty ? null : gistId_);\n        context.showSnackBar(libL10n.success);\n\n        await PrefProps.githubToken.set(token_);\n        if (gistId_.isEmpty) {\n          await PrefProps.gistId.remove();\n        } else {\n          await PrefProps.gistId.set(gistId_);\n        }\n      } catch (e, s) {\n        context.showErrDialog(e, s, 'Gist');\n      }\n    }\n  }\n\n  Future<void> _onTapWebdavSetting(BuildContext context) async {\n    final url = TextEditingController(text: PrefProps.webdavUrl.get());\n    final user = TextEditingController(text: PrefProps.webdavUser.get());\n    final pwd = TextEditingController(text: PrefProps.webdavPwd.get());\n    final nodeUser = FocusNode();\n    final nodePwd = FocusNode();\n    final result = await context.showRoundDialog<bool>(\n      title: 'WebDAV',\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Input(\n            label: 'URL',\n            hint: 'https://example.com/sub/',\n            controller: url,\n            suggestion: false,\n            onSubmitted: (p0) => FocusScope.of(context).requestFocus(nodeUser),\n          ),\n          Input(\n            label: libL10n.user,\n            controller: user,\n            node: nodeUser,\n            suggestion: false,\n            onSubmitted: (p0) => FocusScope.of(context).requestFocus(nodePwd),\n          ),\n          Input(\n            label: libL10n.pwd,\n            controller: pwd,\n            node: nodePwd,\n            suggestion: false,\n            onSubmitted: (_) => context.pop(true),\n          ),\n        ],\n      ),\n      actions: Btnx.oks,\n    );\n    if (result == true) {\n      try {\n        final url_ = url.text;\n        final user_ = user.text;\n        final pwd_ = pwd.text;\n\n        await Webdav.test(url_, user_, pwd_);\n        context.showSnackBar(libL10n.success);\n\n        Webdav.shared.client = WebdavClient.basicAuth(url: url_, user: user_, pwd: pwd_);\n        PrefProps.webdavUrl.set(url_);\n        PrefProps.webdavUser.set(user_);\n        PrefProps.webdavPwd.set(pwd_);\n      } catch (e, s) {\n        context.showErrDialog(e, s, 'Webdav');\n      }\n    }\n  }\n\n  void _onBulkImportServers(BuildContext context) async {\n    final data = await context.showImportDialog(title: libL10n.server, modelDef: Spix.example.toJson());\n    if (data == null) return;\n    String text;\n    try {\n      text = utf8.decode(data);\n    } on FormatException catch (e, s) {\n      context.showErrDialog(e, s, libL10n.error);\n      return;\n    }\n\n    try {\n      final (spis, err) = await context.showLoadingDialog(\n        fn: () => Computer.shared.start((val) {\n          final list = json.decode(val) as List;\n          return list.map((e) => Spi.fromJson(e)).toList();\n        }, text.trim()),\n      );\n      if (err != null || spis == null) return;\n      final sure = await context.showRoundDialog<bool>(\n        title: libL10n.import,\n        child: Text(libL10n.askContinue('${spis.length} ${libL10n.server}')),\n        actions: Btnx.oks,\n      );\n      if (sure == true) {\n        final (suc, err) = await context.showLoadingDialog(\n          fn: () async {\n            final usedIds = <String>{};\n            for (var spi in spis) {\n              // Ensure each server has a unique ID\n\n              // Only generate a new ID if the imported one is empty or already used in importing stage\n              final isIdUsed = spi.id.isNotEmpty || usedIds.contains(spi.id);\n              final spiWithId = isIdUsed ? spi.copyWith(id: ShortId.generate()) : spi;\n              Stores.server.put(spiWithId);\n              usedIds.add(spiWithId.id);\n            }\n            return true;\n          },\n        );\n        if (err != null || suc != true) return;\n        context.showSnackBar(libL10n.success);\n      }\n    } catch (e, s) {\n      context.showErrDialog(e, s, libL10n.import);\n      Loggers.app.warning('Import servers failed', e, s);\n    }\n  }\n\n  Future<bool> _ensureBakPwd(BuildContext context) async {\n    final saved = await SecureStoreProps.bakPwd.read();\n    if (saved != null && saved.isNotEmpty) return true;\n\n    // Show dialog asking if user wants to set password or continue without\n    final result = await context.showRoundDialog<bool>(\n      title: l10n.backupPassword,\n      child: Text(l10n.backupPasswordTip, style: UIs.textGrey),\n      actions: [\n        TextButton(onPressed: () => context.pop(true), child: Text(libL10n.cancel)),\n        TextButton(onPressed: () => context.pop(false), child: Text(libL10n.setting)),\n      ],\n    );\n\n    if (result == true) {\n      // Continue without password\n      return true;\n    } else if (result == false) {\n      // User wants to set password\n      await _onTapSetBakPwd(context);\n      return true; // Allow continuing even if password setting was cancelled\n    }\n\n    return false; // User cancelled the dialog\n  }\n}\n"
  },
  {
    "path": "lib/view/page/container/actions.dart",
    "content": "part of 'container.dart';\n\nextension on _ContainerPageState {\n  /// The notifier for the container state.\n  ContainerNotifier get _containerNotifier => ref.read(_provider.notifier);\n\n  /// Watch the current state of the container.\n  ContainerState get _containerState => ref.watch(_provider);\n\n  Future<void> _showAddFAB() async {\n    final imageCtrl = TextEditingController();\n    final nameCtrl = TextEditingController();\n    final argsCtrl = TextEditingController();\n    await context.showRoundDialog(\n      title: l10n.newContainer,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Input(\n            autoFocus: true,\n            type: TextInputType.text,\n            label: l10n.image,\n            hint: 'xxx:1.1',\n            controller: imageCtrl,\n            suggestion: false,\n          ),\n          Input(\n            type: TextInputType.text,\n            controller: nameCtrl,\n            label: libL10n.name,\n            hint: 'xxx',\n            suggestion: false,\n          ),\n          Input(\n            type: TextInputType.text,\n            controller: argsCtrl,\n            label: l10n.extraArgs,\n            hint: '-p 2222:22 -v ~/.xxx/:/xxx',\n            suggestion: false,\n          ),\n        ],\n      ),\n      actions: Btn.ok(\n        onTap: () async {\n          context.pop();\n          await _showAddCmdPreview(\n            _buildAddCmd(imageCtrl.text.trim(), nameCtrl.text.trim(), argsCtrl.text.trim()),\n          );\n        },\n      ).toList,\n    );\n  }\n\n  Future<void> _showPruneDialog({\n    required String title,\n    String? message,\n    required Future<ContainerErr?> Function() onConfirm,\n  }) async {\n    await context.showRoundDialog(\n      title: title,\n      child: Text(message ?? libL10n.askContinue('${libL10n.prune} $title')),\n      actions: Btn.ok(\n        onTap: () async {\n          context.pop();\n          final (result, err) = await context.showLoadingDialog(fn: onConfirm);\n          if (err != null || result != null) {\n            final e = result?.message ?? err?.toString();\n            context.showRoundDialog(title: libL10n.error, child: Text(e.toString()));\n          } else {\n            context.showSnackBar(libL10n.success);\n          }\n        },\n        red: true,\n      ).toList,\n    );\n  }\n\n  Future<void> _showAddCmdPreview(String cmd) async {\n    await context.showRoundDialog(\n      title: libL10n.preview,\n      child: Text(cmd),\n      actions: [\n        TextButton(onPressed: () => context.pop(), child: Text(libL10n.cancel)),\n        TextButton(\n          onPressed: () async {\n            context.pop();\n\n            final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.run(cmd));\n            if (err != null || result != null) {\n              final e = result?.message ?? err?.toString();\n              context.showRoundDialog(title: libL10n.error, child: Text(e.toString()));\n            }\n          },\n          child: Text(libL10n.run),\n        ),\n      ],\n    );\n  }\n\n  Future<void> _showEditHostDialog() async {\n    final id = widget.args.spi.id;\n    final host = Stores.container.fetch(id);\n    final ctrl = TextEditingController(text: host);\n    await context.showRoundDialog(\n      title: libL10n.edit,\n      child: Input(\n        maxLines: 2,\n        controller: ctrl,\n        onSubmitted: _onSaveDockerHost,\n        hint: 'unix:///run/user/1000/docker.sock',\n        suggestion: false,\n      ),\n      actions: Btn.ok(onTap: () => _onSaveDockerHost(ctrl.text)).toList,\n    );\n  }\n\n  void _onSaveDockerHost(String val) {\n    context.pop();\n    Stores.container.put(widget.args.spi.id, val.trim());\n    _containerNotifier.refresh();\n  }\n\n  void _showImageRmDialog(ContainerImg e) {\n    context.showRoundDialog(\n      title: libL10n.attention,\n      child: Text(libL10n.askContinue('${libL10n.delete} Image(${e.repository})')),\n      actions: Btn.ok(\n        onTap: () async {\n          context.pop();\n          final result = await _containerNotifier.run('rmi ${e.id} -f');\n          if (result != null) {\n            context.showSnackBar(result.message ?? 'null');\n          }\n        },\n        red: true,\n      ).toList,\n    );\n  }\n\n  void _onTapMoreBtn(ContainerMenu item, ContainerPs dItem) async {\n    final id = dItem.id;\n    if (id == null) {\n      context.showSnackBar('Id is null');\n      return;\n    }\n    switch (item) {\n      case ContainerMenu.rm:\n        var force = false;\n        context.showRoundDialog(\n          title: libL10n.attention,\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              Text(libL10n.askContinue('${libL10n.delete} Container(${dItem.name})')),\n              UIs.height13,\n              Row(\n                children: [\n                  StatefulBuilder(\n                    builder: (_, setState) {\n                      return Checkbox(value: force, onChanged: (val) => setState(() => force = val ?? false));\n                    },\n                  ),\n                  Text(libL10n.force),\n                ],\n              ),\n            ],\n          ),\n          actions: Btn.ok(\n            onTap: () async {\n              context.pop();\n\n              final (result, err) = await context.showLoadingDialog(\n                fn: () => _containerNotifier.delete(id, force),\n              );\n              if (err != null || result != null) {\n                final e = result?.message ?? err?.toString();\n                context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));\n              }\n            },\n          ).toList,\n        );\n        break;\n      case ContainerMenu.start:\n        final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.start(id));\n        if (err != null || result != null) {\n          final e = result?.message ?? err?.toString();\n          context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));\n        }\n        break;\n      case ContainerMenu.stop:\n        final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.stop(id));\n        if (err != null || result != null) {\n          final e = result?.message ?? err?.toString();\n          context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));\n        }\n        break;\n      case ContainerMenu.restart:\n        final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.restart(id));\n        if (err != null || result != null) {\n          final e = result?.message ?? err?.toString();\n          context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));\n        }\n        break;\n      case ContainerMenu.logs:\n        final args = SshPageArgs(\n          spi: widget.args.spi,\n          initCmd:\n              '${switch (_containerState.type) {\n                ContainerType.podman => 'podman',\n                ContainerType.docker => 'docker',\n              }} logs -f --tail 100 ${dItem.id}',\n        );\n        SSHPage.route.go(context, args);\n        break;\n      case ContainerMenu.terminal:\n        final args = SshPageArgs(\n          spi: widget.args.spi,\n          initCmd:\n              '${switch (_containerState.type) {\n                ContainerType.podman => 'podman',\n                ContainerType.docker => 'docker',\n              }} exec -it ${dItem.id} sh -c \"command -v bash && exec bash || command -v ash && exec ash || exec sh\"',\n        );\n        SSHPage.route.go(context, args);\n        break;\n    }\n  }\n\n  void _initAutoRefresh() {\n    if (Stores.setting.containerAutoRefresh.fetch()) {\n      Timer.periodic(Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()), (timer) {\n        if (mounted) {\n          _containerNotifier.refresh(isAuto: true);\n        } else {\n          timer.cancel();\n        }\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "lib/view/page/container/container.dart",
    "content": "import 'dart:async';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/route.dart';\nimport 'package:server_box/data/model/app/error.dart';\nimport 'package:server_box/data/model/app/menu/base.dart';\nimport 'package:server_box/data/model/app/menu/container.dart';\nimport 'package:server_box/data/model/container/image.dart';\nimport 'package:server_box/data/model/container/ps.dart';\nimport 'package:server_box/data/model/container/type.dart';\nimport 'package:server_box/data/provider/container.dart';\nimport 'package:server_box/data/provider/server/single.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/view/page/ssh/page/page.dart';\n\npart 'actions.dart';\npart 'types.dart';\n\nclass ContainerPage extends ConsumerStatefulWidget {\n  final SpiRequiredArgs args;\n  const ContainerPage({required this.args, super.key});\n\n  @override\n  ConsumerState<ContainerPage> createState() => _ContainerPageState();\n\n  static const route = AppRouteArg(page: ContainerPage.new, path: '/container');\n}\n\nclass _ContainerPageState extends ConsumerState<ContainerPage> {\n  final _textController = TextEditingController();\n  late final ContainerNotifierProvider _provider;\n\n  @override\n  void dispose() {\n    super.dispose();\n    _textController.dispose();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    final serverState = ref.read(serverProvider(widget.args.spi.id));\n    _provider = containerProvider(\n      serverState.client,\n      widget.args.spi.user,\n      widget.args.spi.id,\n      context,\n    );\n    _initAutoRefresh();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final err = ref.watch(_provider.select((p) => p.error));\n\n    return Scaffold(\n      appBar: _buildAppBar(),\n      body: SafeArea(child: _buildMain()),\n      floatingActionButton: err == null ? _buildFAB() : null,\n    );\n  }\n\n  CustomAppBar _buildAppBar() {\n    return CustomAppBar(\n      centerTitle: true,\n      title: TwoLineText(up: libL10n.container, down: widget.args.spi.name),\n      actions: [\n        IconButton(\n          onPressed: () => context.showLoadingDialog(fn: () => _containerNotifier.refresh()),\n          icon: const Icon(Icons.refresh),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildFAB() {\n    return FloatingActionButton(onPressed: () async => await _showAddFAB(), child: const Icon(Icons.add));\n  }\n\n  Widget _buildMain() {\n    final containerState = _containerState;\n\n    if (containerState.error != null && containerState.items == null) {\n      return SizedBox.expand(\n        child: Column(\n          children: [\n            const Spacer(),\n            const Icon(Icons.error, size: 37),\n            UIs.height13,\n            Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 23),\n              child: Text(containerState.error.toString()),\n            ),\n            const Spacer(),\n            UIs.height13,\n            _buildSettingsBtns,\n          ],\n        ).paddingSymmetric(horizontal: 13),\n      );\n    }\n    if (containerState.items == null || containerState.images == null) {\n      return UIs.centerLoading;\n    }\n\n    return AutoMultiList(\n      children: <Widget>[\n        _buildLoading(containerState),\n        _buildVersion(containerState),\n        _buildPs(containerState),\n        _buildImage(containerState),\n        _buildEmptyStateMessage(containerState),\n        _buildPruneBtns,\n        _buildSettingsBtns,\n      ],\n    );\n  }\n\n  Widget _buildEmptyStateMessage(ContainerState containerState) {\n    final emptyImgs = containerState.images?.isEmpty ?? true;\n    final emptyPs = containerState.items?.isEmpty ?? true;\n    if (emptyPs && emptyImgs && containerState.runLog == null) {\n      return CardX(\n        child: Padding(\n          padding: const EdgeInsets.fromLTRB(17, 17, 17, 7),\n          child: SimpleMarkdown(data: l10n.dockerEmptyRunningItems),\n        ),\n      );\n    }\n    return UIs.placeholder;\n  }\n\n  Widget _buildImage(ContainerState containerState) {\n    return ExpandTile(\n      leading: const Icon(MingCute.clapperboard_line),\n      title: Text(l10n.imagesList),\n      subtitle: Text(l10n.dockerImagesFmt(containerState.images?.length ?? 'null'), style: UIs.textGrey),\n      initiallyExpanded: (containerState.images?.length ?? 0) <= 3,\n      children: containerState.images?.map(_buildImageItem).toList() ?? [],\n    ).cardx;\n  }\n\n  Widget _buildImageItem(ContainerImg e) {\n    final repoSplited = e.repository?.split('/');\n    final title = repoSplited?.lastOrNull ?? e.repository;\n    repoSplited?.removeLast();\n    final reg = repoSplited?.join('/');\n    return ListTile(\n      title: Text(title ?? l10n.unknown, style: UIs.text15),\n      subtitle: Text('${reg ?? ''} - ${e.tag} - ${e.sizeMB}', style: UIs.text13Grey),\n      trailing: Btn.icon(\n        padding: EdgeInsets.zero,\n        icon: const Icon(Icons.delete),\n        onTap: () => _showImageRmDialog(e),\n      ),\n    );\n  }\n\n  Widget _buildLoading(ContainerState containerState) {\n    if (containerState.runLog == null) return UIs.placeholder;\n    return Padding(\n      padding: const EdgeInsets.all(17),\n      child: Column(\n        children: [\n          const Center(child: CircularProgressIndicator()),\n          UIs.height13,\n          Text(containerState.runLog ?? '...'),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildVersion(ContainerState containerState) {\n    return CardX(\n      child: Padding(\n        padding: const EdgeInsets.all(17),\n        child: Row(\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [Text(containerState.type.name.capitalize), Text(containerState.version ?? l10n.unknown)],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildPs(ContainerState containerState) {\n    final items = containerState.items;\n    if (items == null) return UIs.placeholder;\n    final running = items.where((e) => e.status.isRunning).length;\n    final stopped = items.length - running;\n    final subtitle = stopped > 0\n        ? l10n.dockerStatusRunningAndStoppedFmt(running, stopped)\n        : l10n.dockerStatusRunningFmt(running);\n    return ExpandTile(\n      leading: const Icon(OctIcons.container, size: 22),\n      title: Text(libL10n.container),\n      subtitle: Text(subtitle, style: UIs.textGrey),\n      initiallyExpanded: items.length < 7,\n      children: items.map(_buildPsItem).toList(),\n    ).cardx;\n  }\n\n  Widget _buildPsItem(ContainerPs item) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 11),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Row(\n            mainAxisAlignment: MainAxisAlignment.spaceBetween,\n            children: [\n              Text(item.name ?? l10n.unknown, style: UIs.text15),\n              const SizedBox(height: 3),\n              _buildMoreBtn(item),\n            ],\n          ),\n          Text(\n            '${item.image ?? l10n.unknown} - ${switch (item) {\n              final PodmanPs ps => ps.status.displayName,\n              final DockerPs ps => ps.state ?? ps.status.displayName,\n            }}',\n            style: UIs.text13Grey,\n          ),\n          _buildPsItemStats(item),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildPsItemStats(ContainerPs item) {\n    if (item.cpu == null || item.mem == null) return UIs.placeholder;\n    return LayoutBuilder(\n      builder: (_, cons) {\n        final width = cons.maxWidth / 2 - 6.5;\n        return Column(\n          children: [\n            UIs.height13,\n            Row(\n              children: [\n                _buildPsItemStatsItem('CPU', item.cpu, Icons.memory, width: width),\n                UIs.width13,\n                _buildPsItemStatsItem('Net', item.net, Icons.network_cell, width: width),\n              ],\n            ),\n            Row(\n              children: [\n                _buildPsItemStatsItem('Mem', item.mem, Icons.settings_input_component, width: width),\n                UIs.width13,\n                _buildPsItemStatsItem('Disk', item.disk, Icons.storage, width: width),\n              ],\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget _buildPsItemStatsItem(String title, String? value, IconData icon, {required double width}) {\n    return SizedBox(\n      width: width,\n      child: Column(\n        children: [\n          Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              Icon(icon, size: 12, color: Colors.grey),\n              UIs.width7,\n              Expanded(\n                child: Text(\n                  value ?? l10n.unknown,\n                  style: UIs.text11Grey,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildMoreBtn(ContainerPs dItem) {\n    return PopupMenu(\n      items: ContainerMenu.items(dItem.status.isRunning).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),\n      onSelected: (item) => _onTapMoreBtn(item, dItem),\n    );\n  }\n\n  String _buildAddCmd(String image, String name, String args) {\n    var suffix = '';\n    if (args.isEmpty) {\n      suffix = image;\n    } else {\n      suffix = '$args $image';\n    }\n    if (name.isEmpty) {\n      return 'run -itd $suffix';\n    }\n    return 'run -itd --name $name $suffix';\n  }\n\n  Widget get _buildPruneBtns {\n    final len = _PruneTypes.values.length;\n    if (len == 0) return UIs.placeholder;\n    return ExpandTile(\n      leading: const Icon(Icons.delete),\n      title: Text(libL10n.prune),\n      children: _PruneTypes.values.map(_buildPruneBtn).toList(),\n    ).cardx;\n  }\n\n  Widget _buildPruneBtn(_PruneTypes type) {\n    final title = type.name.capitalize;\n    final containerNotifier = _containerNotifier;\n    return ListTile(\n      onTap: () async {\n        await _showPruneDialog(\n          title: title,\n          message: type.tip,\n          onConfirm: switch (type) {\n            _PruneTypes.images => containerNotifier.pruneImages,\n            _PruneTypes.containers => containerNotifier.pruneContainers,\n            _PruneTypes.volumes => containerNotifier.pruneVolumes,\n            _PruneTypes.system => containerNotifier.pruneSystem,\n          },\n        );\n      },\n      title: Text(title),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n    );\n  }\n\n  Widget get _buildSettingsBtns {\n    final len = _SettingsMenuItems.values.length;\n    if (len == 0) return UIs.placeholder;\n    final containerState = _containerState;\n\n    return ExpandTile(\n      leading: const Icon(Icons.settings),\n      title: Text(libL10n.setting),\n      initiallyExpanded: containerState.error != null,\n      children: _SettingsMenuItems.values.map((item) => _buildSettingTile(item, containerState)).toList(),\n    ).cardx;\n  }\n\n  Widget _buildSettingTile(_SettingsMenuItems item, ContainerState containerState) {\n    final String title;\n    switch (item) {\n      case _SettingsMenuItems.editDockerHost:\n        title = '${libL10n.edit} DOCKER_HOST';\n        break;\n      case _SettingsMenuItems.switchProvider:\n        title = containerState.type == ContainerType.podman\n            ? l10n.switchTo('Docker')\n            : l10n.switchTo('Podman');\n        break;\n    }\n    return ListTile(\n      onTap: () {\n        switch (item) {\n          case _SettingsMenuItems.editDockerHost:\n            _showEditHostDialog();\n            break;\n          case _SettingsMenuItems.switchProvider:\n            ref\n                .read(_provider.notifier)\n                .setType(\n                  containerState.type == ContainerType.docker ? ContainerType.podman : ContainerType.docker,\n                );\n            break;\n        }\n      },\n      title: Text(title),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/container/types.dart",
    "content": "part of 'container.dart';\n\nenum _SettingsMenuItems { editDockerHost, switchProvider }\n\nenum _PruneTypes {\n  images,\n  containers,\n  volumes,\n  system;\n\n  String? get tip {\n    return switch (this) {\n      _PruneTypes.system =>\n        'This will remove all unused data, including images, containers, volumes, and networks.',\n      _ => null,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/view/page/home.dart",
    "content": "import 'dart:io';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/foundation.dart' show kReleaseMode;\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:responsive_framework/responsive_framework.dart';\nimport 'package:server_box/core/chan.dart';\nimport 'package:server_box/core/sync.dart';\nimport 'package:server_box/data/model/app/menu/platform.dart';\nimport 'package:server_box/data/model/app/tab.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/res/build_data.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/data/res/url.dart';\nimport 'package:server_box/view/page/setting/entry.dart';\nimport 'package:wakelock_plus/wakelock_plus.dart';\n\nclass HomePage extends ConsumerStatefulWidget {\n  const HomePage({super.key});\n\n  @override\n  ConsumerState<HomePage> createState() => _HomePageState();\n\n  static const route = AppRouteNoArg(page: HomePage.new, path: '/');\n}\n\nclass _HomePageState extends ConsumerState<HomePage>\n    with AutomaticKeepAliveClientMixin, AfterLayoutMixin, WidgetsBindingObserver, GlobalRef {\n  late final PageController _pageController;\n\n  final _selectIndex = ValueNotifier(0);\n\n  bool _switchingPage = false;\n  bool _shouldAuth = false;\n  DateTime? _pausedTime;\n\n  late final _notifier = ref.read(serversProvider.notifier);\n  late final _provider = ref.read(serversProvider);\n  late List<AppTab> _tabs = Stores.setting.homeTabs.fetch();\n\n  @override\n  void dispose() {\n    super.dispose();\n    WidgetsBinding.instance.removeObserver(this);\n    // In release builds (real app exit), close connections.\n    // In debug (hot reload), avoid forcing disconnects.\n    if (kReleaseMode) {\n      Future(() => _notifier.closeServer());\n    }\n    _pageController.dispose();\n    WakelockPlus.disable();\n\n    _selectIndex.dispose();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    SystemUIs.switchStatusBar(hide: false);\n    WidgetsBinding.instance.addObserver(this);\n    // avoid index out of range\n    if (_selectIndex.value >= _tabs.length || _selectIndex.value < 0) {\n      _selectIndex.value = 0;\n    }\n    _pageController = PageController(initialPage: _selectIndex.value);\n    if (Stores.setting.generalWakeLock.fetch()) {\n      WakelockPlus.enable();\n    }\n\n    // Listen to homeTabs changes\n    Stores.setting.homeTabs.listenable().addListener(() {\n      final newTabs = Stores.setting.homeTabs.fetch();\n      if (mounted && newTabs != _tabs) {\n        setState(() {\n          _tabs = newTabs;\n          // Ensure current page index is valid\n          if (_selectIndex.value >= _tabs.length) {\n            _selectIndex.value = _tabs.length - 1;\n          }\n          if (_selectIndex.value < 0 && _tabs.isNotEmpty) {\n            _selectIndex.value = 0;\n          }\n        });\n      }\n    });\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    super.didChangeAppLifecycleState(state);\n    if (isDesktop) return;\n\n    switch (state) {\n      case AppLifecycleState.resumed:\n        if (_shouldAuth) {\n          final delay = Stores.setting.delayBioAuthLock.fetch();\n          if (delay > 0 && _pausedTime != null) {\n            final now = DateTime.now();\n            if (now.difference(_pausedTime ?? now).inSeconds > delay) {\n              _goAuth();\n            } else {\n              _shouldAuth = false;\n            }\n            _pausedTime = null;\n          } else {\n            _goAuth();\n          }\n        }\n        final serverNotifier = _notifier;\n        if (_provider.autoRefreshTimer == null) {\n          serverNotifier.startAutoRefresh();\n        }\n        MethodChans.updateHomeWidget();\n        break;\n      case AppLifecycleState.paused:\n        _pausedTime = DateTime.now();\n        _shouldAuth = true;\n        // Keep running in background on Android device\n        if (isAndroid && Stores.setting.bgRun.fetch()) {\n          // Keep this if statement single\n          // if (Pros.app.moveBg) {\n          //   BgRunMC.moveToBg();\n          // }\n        } else {\n          //Pros.server.setDisconnected();\n          _notifier.stopAutoRefresh();\n        }\n        break;\n      default:\n        break;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    final isMobile = ResponsiveBreakpoints.of(context).isMobile;\n\n    final Widget mainContent = Scaffold(\n      appBar: _AppBar(MediaQuery.paddingOf(context).top),\n      body: Row(\n        children: [\n          if (!isMobile) _buildRailBar(),\n          Expanded(\n            child: PageView.builder(\n              controller: _pageController,\n              itemCount: _tabs.length,\n              physics: const NeverScrollableScrollPhysics(),\n              itemBuilder: (_, index) => _tabs[index].page,\n              onPageChanged: (value) {\n                FocusScope.of(context).unfocus();\n                if (!_switchingPage) {\n                  _selectIndex.value = value;\n                }\n              },\n            ),\n          ),\n        ],\n      ),\n      bottomNavigationBar: isMobile ? _buildBottomBar() : null,\n    );\n\n    if (Platform.isMacOS) {\n      return PlatformMenuBar(\n        menus: MacOSMenuBarManager.buildMenuBar(context, (int index) {\n          _onDestinationSelected(index);\n        }),\n        child: mainContent,\n      );\n    }\n    return mainContent;\n  }\n\n  Widget _buildBottomBar() {\n    if (Stores.setting.fullScreen.fetch()) return UIs.placeholder;\n    return ListenableBuilder(\n      listenable: _selectIndex,\n      builder: (context, child) => NavigationBar(\n        selectedIndex: _selectIndex.value,\n        height: kBottomNavigationBarHeight * 1.1,\n        animationDuration: const Duration(milliseconds: 250),\n        onDestinationSelected: _onDestinationSelected,\n        labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,\n        destinations: _tabs.map((tab) => tab.navDestination).toList(),\n      ),\n    );\n  }\n\n  Widget _buildRailBar({bool extended = false}) {\n    final fullscreen = Stores.setting.fullScreen.fetch();\n    if (fullscreen) return UIs.placeholder;\n\n    return Stack(\n      children: [\n        _selectIndex.listenVal(\n          (idx) => NavigationRail(\n            extended: extended,\n            minExtendedWidth: 150,\n            leading: extended ? const SizedBox(height: 20) : null,\n            trailing: extended ? const SizedBox(height: 20) : null,\n            labelType: extended ? NavigationRailLabelType.none : NavigationRailLabelType.all,\n            selectedIndex: idx,\n            destinations: _tabs.map((tab) => tab.navRailDestination).toList(),\n            onDestinationSelected: _onDestinationSelected,\n          ),\n        ),\n        // Settings Btn\n        Positioned(\n          bottom: 10,\n          left: 0,\n          right: 0,\n          child: IconButton(\n            icon: const Icon(Icons.settings),\n            tooltip: libL10n.setting,\n            onPressed: () {\n              SettingsPage.route.go(context);\n            },\n          ),\n        ),\n      ],\n    );\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Future<void> afterFirstLayout(BuildContext context) async {\n    // Auth required for first launch\n    _goAuth();\n\n    //_reqNotiPerm();\n\n    if (Stores.setting.autoCheckAppUpdate.fetch()) {\n      AppUpdateIface.doUpdate(build: BuildData.build, url: Urls.updateCfg, context: context);\n    }\n    MethodChans.updateHomeWidget();\n    await _notifier.refresh();\n\n    bakSync.sync(milliDelay: 1000);\n  }\n\n  // Future<void> _reqNotiPerm() async {\n  //   if (!isAndroid) return;\n  //   final suc = await PermUtils.request(Permission.notification);\n  //   if (!suc) {\n  //     final noNotiPerm = Stores.setting.noNotiPerm;\n  //     context.showRoundDialog(\n  //       title: l10n.error,\n  //       child: Text(l10n.noNotiPerm),\n  //       actions: [\n  //         TextButton(\n  //           onPressed: () {\n  //             noNotiPerm.put(true);\n  //             context.pop();\n  //           },\n  //     if (noNotiPerm.fetch()) return;\n  //           child: Text(l10n.ok),\n  //         ),\n  //       ],\n  //     );\n  //   }\n  // }\n\n  void _goAuth() {\n    if (Stores.setting.useBioAuth.fetch()) {\n      if (LocalAuthPage.route.alreadyIn) return;\n      LocalAuthPage.route.go(context, args: LocalAuthPageArgs(onAuthSuccess: () => _shouldAuth = false));\n    }\n  }\n\n  void _onDestinationSelected(int index) {\n    if (_selectIndex.value == index) return;\n    if (index < 0 || index >= _tabs.length) return;\n    _selectIndex.value = index;\n    _switchingPage = true;\n    _pageController.animateToPage(\n      index,\n      duration: const Duration(milliseconds: 677),\n      curve: Curves.fastLinearToSlowEaseIn,\n    );\n    Future.delayed(const Duration(milliseconds: 677), () {\n      _switchingPage = false;\n    });\n  }\n}\n\nfinal class _AppBar extends StatelessWidget implements PreferredSizeWidget {\n  final double paddingTop;\n\n  const _AppBar(this.paddingTop);\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(height: preferredSize.height);\n  }\n\n  @override\n  Size get preferredSize {\n    return Size.fromHeight(paddingTop);\n  }\n}\n"
  },
  {
    "path": "lib/view/page/iperf.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/core/route.dart';\nimport 'package:server_box/view/page/ssh/page/page.dart';\n\nclass IPerfPage extends StatefulWidget {\n  final SpiRequiredArgs args;\n\n  const IPerfPage({super.key, required this.args});\n\n  @override\n  State<IPerfPage> createState() => _IPerfPageState();\n\n  static const route = AppRouteArg<void, SpiRequiredArgs>(page: IPerfPage.new, path: '/iperf');\n}\n\nclass _IPerfPageState extends State<IPerfPage> {\n  final _hostCtrl = TextEditingController();\n  final _portCtrl = TextEditingController();\n\n  @override\n  void dispose() {\n    super.dispose();\n    _hostCtrl.dispose();\n    _portCtrl.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(title: const Text('iperf')),\n      body: _buildBody(),\n      floatingActionButton: _buildFAB(),\n    );\n  }\n\n  Widget _buildFAB() {\n    return FloatingActionButton(\n      heroTag: 'iperf',\n      child: const Icon(Icons.send),\n      onPressed: () {\n        if (_hostCtrl.text.isEmpty || _portCtrl.text.isEmpty) {\n          context.showSnackBar(libL10n.empty);\n          return;\n        }\n        final args = SshPageArgs(\n          spi: widget.args.spi,\n          initCmd: 'iperf -c ${_hostCtrl.text} -p ${_portCtrl.text}',\n        );\n        SSHPage.route.go(context, args);\n      },\n    );\n  }\n\n  Widget _buildBody() {\n    return ListView(\n      padding: const EdgeInsets.symmetric(horizontal: 17),\n      children: [\n        Input(controller: _hostCtrl, label: libL10n.host, icon: Icons.computer, suggestion: false),\n        Input(\n          controller: _portCtrl,\n          label: libL10n.port,\n          type: TextInputType.number,\n          icon: Icons.numbers,\n          suggestion: false,\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/ping.dart",
    "content": "import 'dart:async';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/server/ping_result.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/provider/server/single.dart';\n\n/// Only permit ipv4 / ipv6 / domain chars\nfinal targetReg = RegExp(r'[a-zA-Z0-9\\.-_:]+');\n\nclass PingPage extends ConsumerStatefulWidget {\n  const PingPage({super.key});\n\n  @override\n  ConsumerState<PingPage> createState() => _PingPageState();\n\n  static const route = AppRouteNoArg(page: PingPage.new, path: '/ping');\n}\n\nclass _PingPageState extends ConsumerState<PingPage> with AutomaticKeepAliveClientMixin {\n  late TextEditingController _textEditingController;\n  final _results = ValueNotifier(<PingResult>[]);\n  bool get isInit => _results.value.isEmpty;\n\n  @override\n  void dispose() {\n    super.dispose();\n    _textEditingController.dispose();\n    _results.dispose();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _textEditingController = TextEditingController(text: '');\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return Scaffold(body: _results.listenVal(_buildBody), floatingActionButton: _buildFAB());\n  }\n\n  Widget _buildFAB() {\n    return FloatingActionButton(\n      heroTag: 'ping',\n      onPressed: () {\n        context.showRoundDialog(\n          title: libL10n.select,\n          child: Input(\n            autoFocus: true,\n            controller: _textEditingController,\n            hint: 'example.com',\n            maxLines: 2,\n            minLines: 1,\n            onSubmitted: (_) => _doPing(),\n          ),\n          actions: Btn.ok(onTap: _doPing).toList,\n        );\n      },\n      child: const Icon(Icons.search),\n    );\n  }\n\n  Future<void> _doPing() async {\n    context.pop();\n    try {\n      await doPing();\n    } catch (e) {\n      context.showRoundDialog(\n        title: libL10n.error,\n        child: Text(e.toString()),\n        actions: [TextButton(onPressed: () => Pfs.copy(e.toString()), child: Text(libL10n.copy))],\n      );\n      rethrow;\n    }\n  }\n\n  Widget _buildBody(List<PingResult> results) {\n    if (isInit) {\n      return Center(child: Text(libL10n.empty));\n    }\n    return ListView.builder(\n      padding: const EdgeInsets.all(11),\n      controller: ScrollController(),\n      itemCount: results.length,\n      itemBuilder: (_, index) => _buildResultItem(results[index]),\n    );\n  }\n\n  Widget _buildResultItem(PingResult result) {\n    final unknown = l10n.unknown;\n    final ms = libL10n.ms;\n    return CardX(\n      child: ListTile(\n        contentPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),\n        title: Text(\n          result.serverName,\n          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: UIs.primaryColor),\n        ),\n        subtitle: Text(_buildPingSummary(result, unknown, ms), style: UIs.text11),\n        trailing: Text(\n          '${libL10n.pingAvg}${result.statistic?.avg?.toStringAsFixed(2) ?? l10n.unknown} $ms',\n          style: TextStyle(fontSize: 14, color: UIs.primaryColor),\n        ),\n      ),\n    );\n  }\n\n  String _buildPingSummary(PingResult result, String unknown, String ms) {\n    final ip = result.ip ?? unknown;\n    if (result.results == null || result.results!.isEmpty) {\n      return '$ip - ${libL10n.empty}';\n    }\n    final ttl = result.results?.firstOrNull?.ttl ?? unknown;\n    final loss = result.statistic?.loss ?? unknown;\n    final min = result.statistic?.min ?? unknown;\n    final max = result.statistic?.max ?? unknown;\n    return '$ip\\n${libL10n.ttl}: $ttl, ${libL10n.loss}: $loss%\\n${l10n.min}: $min $ms, ${l10n.max}: $max $ms';\n  }\n\n  Future<void> doPing() async {\n    FocusScope.of(context).requestFocus(FocusNode());\n    _results.value.clear();\n    final target = _textEditingController.text.trim();\n    if (target.isEmpty) {\n      context.showSnackBar(l10n.pingInputIP);\n      return;\n    }\n\n    if (ref.read(serversProvider).serverOrder.isEmpty) {\n      context.showSnackBar(l10n.pingNoServer);\n      return;\n    }\n\n    /// avoid ping command injection\n    if (!targetReg.hasMatch(target)) {\n      context.showSnackBar(l10n.pingInputIP);\n      return;\n    }\n\n    await Future.wait(\n      ref.read(serversProvider).servers.values.map((spi) async {\n        final serverState = ref.read(serverProvider(spi.id));\n        if (serverState.client == null) {\n          return;\n        }\n        final result = await serverState.client!.run('ping -c 3 $target').string;\n        _results.value.add(PingResult.parse(spi.name, result));\n        // [ValueNotifier] only notify when value is changed\n        // But we just add a element to list without changing the list itself\n        // So we need to notify manually\n        //\n        // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member\n        _results.notifyListeners();\n      }),\n    );\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n}\n"
  },
  {
    "path": "lib/view/page/private_key/edit.dart",
    "content": "import 'dart:io';\n\nimport 'package:computer/computer.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/utils/server.dart';\nimport 'package:server_box/data/model/server/private_key_info.dart';\nimport 'package:server_box/data/provider/private_key.dart';\nimport 'package:server_box/data/res/misc.dart';\n\nconst _format = 'text/plain';\n\nfinal class PrivateKeyEditPageArgs {\n  final PrivateKeyInfo? pki;\n  const PrivateKeyEditPageArgs({this.pki});\n}\n\nclass PrivateKeyEditPage extends ConsumerStatefulWidget {\n  final PrivateKeyEditPageArgs? args;\n  const PrivateKeyEditPage({super.key, this.args});\n\n  @override\n  ConsumerState<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();\n\n  static const route = AppRoute(page: PrivateKeyEditPage.new, path: '/private_key/edit');\n}\n\nclass _PrivateKeyEditPageState extends ConsumerState<PrivateKeyEditPage> {\n  final _nameController = TextEditingController();\n  final _keyController = TextEditingController();\n  final _pwdController = TextEditingController();\n  final _nameNode = FocusNode();\n  final _keyNode = FocusNode();\n  final _pwdNode = FocusNode();\n\n  late FocusScopeNode _focusScope;\n\n  final _loading = ValueNotifier<Widget?>(null);\n\n  late final _notifier = ref.read(privateKeyProvider.notifier);\n\n  PrivateKeyInfo? get pki => widget.args?.pki;\n\n  @override\n  void dispose() {\n    super.dispose();\n    _nameController.dispose();\n    _keyController.dispose();\n    _pwdController.dispose();\n    _nameNode.dispose();\n    _keyNode.dispose();\n    _pwdNode.dispose();\n    _loading.dispose();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    final pki = this.pki;\n    if (pki != null) {\n      _nameController.text = pki.id;\n      _keyController.text = pki.key;\n    } else {\n      Clipboard.getData(_format).then((value) {\n        if (value == null) return;\n        final clipdata = value.text?.trim() ?? '';\n        if (clipdata.startsWith('-----BEGIN') && clipdata.endsWith('-----')) {\n          _keyController.text = clipdata;\n        }\n      });\n    }\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    _focusScope = FocusScope.of(context);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(appBar: _buildAppBar(), body: _buildBody(), floatingActionButton: _buildFAB());\n  }\n\n  CustomAppBar _buildAppBar() {\n    final pki = this.pki;\n    final actions = pki != null\n        ? [\n            IconButton(\n              tooltip: libL10n.delete,\n              onPressed: () {\n                context.showRoundDialog(\n                  title: libL10n.attention,\n                  child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.privateKey}(${pki.id})')),\n                  actions: Btn.ok(\n                    onTap: () {\n                      _notifier.delete(pki);\n                      context.pop();\n                      context.pop();\n                    },\n                    red: true,\n                  ).toList,\n                );\n              },\n              icon: const Icon(Icons.delete),\n            ),\n          ]\n        : null;\n    return CustomAppBar(title: Text(libL10n.edit), actions: actions);\n  }\n\n  String _standardizeLineSeparators(String value) {\n    return value.replaceAll('\\r\\n', '\\n').replaceAll('\\r', '\\n');\n  }\n\n  Widget _buildFAB() {\n    return FloatingActionButton(tooltip: l10n.save, onPressed: _onTapSave, child: const Icon(Icons.save));\n  }\n\n  Widget _buildBody() {\n    return AutoMultiList(\n      children: [\n        Input(\n          autoFocus: true,\n          controller: _nameController,\n          type: TextInputType.text,\n          node: _nameNode,\n          onSubmitted: (_) => _focusScope.requestFocus(_keyNode),\n          label: libL10n.name,\n          icon: Icons.info,\n          suggestion: true,\n        ),\n        Input(\n          controller: _keyController,\n          minLines: 3,\n          maxLines: 10,\n          type: TextInputType.text,\n          node: _keyNode,\n          onSubmitted: (_) => _focusScope.requestFocus(_pwdNode),\n          label: l10n.privateKey,\n          icon: Icons.vpn_key,\n          suggestion: false,\n        ),\n        TextButton(\n          onPressed: () async {\n            final path = await Pfs.pickFilePath();\n            if (path == null) return;\n\n            final file = File(path);\n            if (!file.existsSync()) {\n              context.showSnackBar(libL10n.notExistFmt(path));\n              return;\n            }\n            final size = (await file.stat()).size;\n            if (size > Miscs.privateKeyMaxSize) {\n              context.showSnackBar(\n                l10n.fileTooLarge(path, size.bytes2Str, Miscs.privateKeyMaxSize.bytes2Str),\n              );\n              return;\n            }\n\n            final content = await file.readAsString();\n            // dartssh2 accepts only LF (but not CRLF or CR)\n            _keyController.text = _standardizeLineSeparators(content.trim());\n          },\n          child: Text(libL10n.file),\n        ),\n        Input(\n          controller: _pwdController,\n          type: TextInputType.text,\n          node: _pwdNode,\n          obscureText: true,\n          label: libL10n.pwd,\n          icon: Icons.password,\n          suggestion: false,\n          onSubmitted: (_) => _onTapSave(),\n        ),\n        SizedBox(height: MediaQuery.of(context).size.height * 0.1),\n        ValBuilder(listenable: _loading, builder: (val) => val ?? UIs.placeholder),\n      ],\n    );\n  }\n\n  void _onTapSave() async {\n    final name = _nameController.text;\n    final key = _standardizeLineSeparators(_keyController.text.trim());\n    final pwd = _pwdController.text;\n    if (name.isEmpty || key.isEmpty) {\n      context.showSnackBar(libL10n.empty);\n      return;\n    }\n    FocusScope.of(context).unfocus();\n    _loading.value = SizedLoading.medium;\n    try {\n      final decrypted = await Computer.shared.start(decyptPem, [key, pwd]);\n      final pki = PrivateKeyInfo(id: name, key: decrypted);\n      final originPki = this.pki;\n      if (originPki != null) {\n        _notifier.update(originPki, pki);\n      } else {\n        _notifier.add(pki);\n      }\n    } catch (e) {\n      context.showSnackBar(e.toString());\n      rethrow;\n    } finally {\n      _loading.value = null;\n    }\n    context.pop();\n  }\n}\n"
  },
  {
    "path": "lib/view/page/private_key/list.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/server/private_key_info.dart';\nimport 'package:server_box/data/provider/private_key.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/view/page/private_key/edit.dart';\n\nclass PrivateKeysListPage extends ConsumerStatefulWidget {\n  const PrivateKeysListPage({super.key});\n\n  @override\n  ConsumerState<PrivateKeysListPage> createState() => _PrivateKeyListState();\n\n  static const route = AppRouteNoArg(page: PrivateKeysListPage.new, path: '/private_key');\n}\n\nclass _PrivateKeyListState extends ConsumerState<PrivateKeysListPage> with AfterLayoutMixin {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: SafeArea(child: _buildBody()),\n      floatingActionButton: FloatingActionButton(\n        child: const Icon(Icons.add),\n        onPressed: () => PrivateKeyEditPage.route.go(context),\n      ),\n    );\n  }\n\n  Widget _buildBody() {\n    final privateKeyState = ref.watch(privateKeyProvider);\n    final pkis = privateKeyState.keys;\n    \n    if (pkis.isEmpty) {\n      return Center(child: Text(libL10n.empty));\n    }\n\n    final children = pkis.map(_buildKeyItem).toList();\n    return AutoMultiList(children: children);\n  }\n\n  Widget _buildKeyItem(PrivateKeyInfo item) {\n    return ListTile(\n      title: Text(item.id),\n      subtitle: Text(item.type ?? l10n.unknown, style: UIs.textGrey),\n      onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: item)),\n      trailing: const Icon(Icons.edit),\n    ).cardx;\n  }\n\n  @override\n  FutureOr<void> afterFirstLayout(BuildContext context) {\n    _autoAddSystemPriavteKey();\n  }\n}\n\nextension on _PrivateKeyListState {\n  void _autoAddSystemPriavteKey() async {\n    // Only trigger on desktop platform and no private key saved\n    if (isDesktop && Stores.snippet.box.keys.isEmpty) {\n      final home = Pfs.homeDir;\n      if (home == null) return;\n      final idRsaFile = File(home.joinPath('.ssh/id_rsa'));\n      if (!idRsaFile.existsSync()) return;\n      final sysPk = PrivateKeyInfo(id: 'system', key: await idRsaFile.readAsString());\n      context.showRoundDialog(\n        title: libL10n.attention,\n        child: Text(l10n.addSystemPrivateKeyTip),\n        actions: Btn.ok(\n          onTap: () {\n            context.pop();\n            PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: sysPk));\n          },\n        ).toList,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "lib/view/page/process.dart",
    "content": "import 'dart:async';\n\nimport 'package:dartssh2/dartssh2.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/route.dart';\nimport 'package:server_box/data/model/app/scripts/shell_func.dart';\nimport 'package:server_box/data/model/server/proc.dart';\nimport 'package:server_box/data/provider/server/single.dart';\nimport 'package:server_box/data/res/store.dart';\n\nclass ProcessPage extends ConsumerStatefulWidget {\n  final SpiRequiredArgs args;\n\n  const ProcessPage({super.key, required this.args});\n\n  @override\n  ConsumerState<ProcessPage> createState() => _ProcessPageState();\n\n  static const route = AppRouteArg(page: ProcessPage.new, path: '/process');\n}\n\nclass _ProcessPageState extends ConsumerState<ProcessPage> {\n  late Timer _timer;\n  late MediaQueryData _media;\n\n  SSHClient? _client;\n\n  PsResult _result = const PsResult(procs: []);\n  bool _checkedIncompleteData = false;\n\n  // Issue #64\n  // In cpu mode, the process list will change in a high frequency.\n  // So user will easily know that the list is refreshed.\n  ProcSortMode _procSortMode = ProcSortMode.cpu;\n  final _sortModes = List<ProcSortMode>.from(ProcSortMode.values);\n\n  late final _provider = serverProvider(widget.args.spi.id);\n\n  @override\n  void dispose() {\n    super.dispose();\n    _timer.cancel();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    final serverState = ref.read(_provider);\n    _client = serverState.client;\n    final duration = Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());\n    _timer = Timer.periodic(duration, (_) => _refresh());\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    _media = MediaQuery.of(context);\n  }\n\n  Future<void> _refresh() async {\n    if (mounted) {\n      final serverState = ref.read(_provider);\n      final systemType = serverState.status.system;\n      final result = await _client\n          ?.run(ShellFunc.process.exec(widget.args.spi.id, systemType: systemType, customDir: null))\n          .string;\n      if (result == null || result.isEmpty) {\n        context.showSnackBar(libL10n.empty);\n        return;\n      }\n      _result = PsResult.parse(result, sort: _procSortMode);\n\n      if (!_checkedIncompleteData) {\n        final isAnyProcDataNotComplete = _result.procs.any((e) => e.cpu == null || e.mem == null);\n        if (isAnyProcDataNotComplete) {\n          _sortModes.removeWhere((e) => e == ProcSortMode.cpu || e == ProcSortMode.mem);\n        }\n        _checkedIncompleteData = true;\n      }\n      setState(() {});\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final actions = <Widget>[\n      PopupMenuButton<ProcSortMode>(\n        onSelected: (value) {\n          setState(() {\n            _procSortMode = value;\n          });\n        },\n        icon: const Icon(Icons.sort),\n        initialValue: _procSortMode,\n        itemBuilder: (_) => _sortModes.map((e) => PopupMenuItem(value: e, child: Text(e.name))).toList(),\n      ),\n    ];\n    if (_result.error != null) {\n      actions.add(\n        IconButton(\n          icon: const Icon(Icons.error),\n          onPressed: () => context.showRoundDialog(\n            title: libL10n.error,\n            child: SingleChildScrollView(child: Text(_result.error!)),\n            actions: [TextButton(onPressed: () => Pfs.copy(_result.error!), child: Text(libL10n.copy))],\n          ),\n        ),\n      );\n    }\n    Widget child;\n    if (_result.procs.isEmpty) {\n      child = UIs.centerLoading;\n    } else {\n      child = ListView.builder(\n        itemCount: _result.procs.length,\n        padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),\n        itemBuilder: (_, idx) => _buildListItem(_result.procs[idx]),\n      );\n    }\n    return Scaffold(\n      appBar: CustomAppBar(\n        centerTitle: true,\n        title: TwoLineText(up: widget.args.spi.name, down: libL10n.process),\n        actions: actions,\n      ),\n      body: child,\n    );\n  }\n\n  Widget _buildListItem(Proc proc) {\n    final leading = proc.user == null\n        ? Text(proc.pid.toString())\n        : TwoLineText(up: proc.pid.toString(), down: proc.user!);\n    return CardX(\n      key: ValueKey(proc.pid),\n      child: ListTile(\n        leading: SizedBox(width: _media.size.width / 6, child: leading),\n        title: Text(proc.binary),\n        subtitle: Text(proc.command, style: UIs.textGrey, maxLines: 3, overflow: TextOverflow.fade),\n        trailing: _buildItemTrail(proc),\n      ),\n    );\n  }\n\n  Widget _buildItemTrail(Proc proc) {\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        if (proc.cpu != null) TwoLineText(up: proc.cpu!.toStringAsFixed(1), down: 'cpu'),\n        if (proc.cpu != null && proc.mem != null) UIs.width13,\n        if (proc.mem != null) TwoLineText(up: proc.mem!.toStringAsFixed(1), down: 'mem'),\n        if (proc.cpu != null || proc.mem != null) UIs.width13,\n        IconButton(\n          icon: const Icon(Icons.stop),\n          onPressed: () {\n            context.showRoundDialog(\n              title: libL10n.attention,\n              child: Text(libL10n.askContinue('${libL10n.stop} ${libL10n.process}(${proc.pid})')),\n              actions: [\n                Btn.cancel(),\n                Btn.ok(\n                  onTap: () async {\n                    context.pop();\n                    await context.showLoadingDialog(\n                      fn: () async {\n                        await _client?.run('kill ${proc.pid}');\n                        await _refresh();\n                      },\n                    );\n                  },\n                ),\n              ],\n            );\n          },\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/pve.dart",
    "content": "import 'dart:async';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/server/pve.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/provider/pve.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/view/widget/percent_circle.dart';\n\nfinal class PvePageArgs {\n  final Spi spi;\n\n  const PvePageArgs({required this.spi});\n}\n\nfinal class PvePage extends ConsumerStatefulWidget {\n  final PvePageArgs args;\n\n  const PvePage({super.key, required this.args});\n\n  @override\n  ConsumerState<PvePage> createState() => _PvePageState();\n\n  static const route = AppRouteArg<void, PvePageArgs>(page: PvePage.new, path: '/pve');\n}\n\nconst _kHorziPadding = 11.0;\n\nfinal class _PvePageState extends ConsumerState<PvePage> {\n  late MediaQueryData _media;\n  Timer? _timer;\n\n  late final _provider = pveProvider(widget.args.spi);\n  late final _notifier = ref.read(_provider.notifier);\n\n  @override\n  void dispose() {\n    super.dispose();\n    _timer?.cancel();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    _media = MediaQuery.of(context);\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _initRefreshTimer();\n    _afterInit();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final pveState = ref.watch(_provider);\n\n    // If there is an error, stop the timer\n    if (pveState.error != null) {\n      _timer?.cancel();\n    }\n\n    return Scaffold(\n      appBar: CustomAppBar(\n        title: TwoLineText(up: 'PVE', down: widget.args.spi.name),\n        actions: [\n          pveState.error == null\n              ? UIs.placeholder\n              : Btn.icon(\n                  icon: const Icon(Icons.refresh),\n                  onTap: () {\n                    _notifier.list();\n                    _initRefreshTimer();\n                  },\n                ),\n        ],\n      ),\n      body: pveState.error != null\n          ? Padding(\n              padding: const EdgeInsets.all(13),\n              child: Center(child: Text(pveState.error.toString())),\n            )\n          : _buildBody(pveState.data),\n    );\n  }\n\n  Widget _buildBody(PveRes? data) {\n    if (data == null) {\n      return UIs.centerLoading;\n    }\n\n    PveResType? lastType;\n    return ListView.builder(\n      padding: const EdgeInsets.symmetric(horizontal: _kHorziPadding, vertical: 7),\n      itemCount: data.length * 2,\n      itemBuilder: (context, index) {\n        final item = data[index ~/ 2];\n        if (index % 2 == 0) {\n          final type = switch (item) {\n            final PveNode _ => PveResType.node,\n            final PveQemu _ => PveResType.qemu,\n            final PveLxc _ => PveResType.lxc,\n            final PveStorage _ => PveResType.storage,\n            final PveSdn _ => PveResType.sdn,\n          };\n          if (type == lastType) {\n            return UIs.placeholder;\n          }\n          lastType = type;\n          return Padding(\n            padding: const EdgeInsets.symmetric(vertical: 7),\n            child: Align(\n              alignment: Alignment.center,\n              child: Text(\n                type.toStr,\n                style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),\n                textAlign: TextAlign.start,\n              ),\n            ),\n          );\n        }\n        return switch (item) {\n          final PveNode _ => _buildNode(item),\n          final PveQemu _ => _buildQemu(item),\n          final PveLxc _ => _buildLxc(item),\n          final PveStorage _ => _buildStorage(item),\n          final PveSdn _ => _buildSdn(item),\n        };\n      },\n    );\n  }\n\n  Widget _buildNode(PveNode item) {\n    final valueAnim = AlwaysStoppedAnimation(UIs.primaryColor);\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 13),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Row(\n            children: [\n              Text(item.node, style: UIs.text15Bold),\n              const Spacer(),\n              Text(item.topRight, style: UIs.text12Grey),\n            ],\n          ),\n          UIs.height13,\n          // Row(\n          //   mainAxisAlignment: MainAxisAlignment.spaceAround,\n          //   children: [\n          //     _wrap(PercentCircle(percent: item.cpu / item.maxcpu), 3),\n          //     _wrap(PercentCircle(percent: item.mem / item.maxmem), 3),\n          //   ],\n          // ),\n          Row(\n            children: [\n              const Icon(Icons.memory, size: 13, color: Colors.grey),\n              UIs.width7,\n              const Text('CPU', style: UIs.text12Grey),\n              const Spacer(),\n              Text('${(item.cpu * 100).toStringAsFixed(1)} %', style: UIs.text12Grey),\n            ],\n          ),\n          const SizedBox(height: 3),\n          LinearProgressIndicator(value: item.cpu / item.maxcpu, minHeight: 7, valueColor: valueAnim),\n          UIs.height7,\n          Row(\n            children: [\n              const Icon(Icons.view_agenda, size: 13, color: Colors.grey),\n              UIs.width7,\n              const Text('RAM', style: UIs.text12Grey),\n              const Spacer(),\n              Text('${item.mem.bytes2Str} / ${item.maxmem.bytes2Str}', style: UIs.text12Grey),\n            ],\n          ),\n          const SizedBox(height: 3),\n          LinearProgressIndicator(value: item.mem / item.maxmem, minHeight: 7, valueColor: valueAnim),\n        ],\n      ),\n    ).cardx;\n  }\n\n  Widget _buildQemu(PveQemu item) {\n    if (!item.available) {\n      return ListTile(\n        title: Text(_wrapNodeName(item), style: UIs.text13Bold),\n        trailing: _buildCtrlBtns(item),\n      ).cardx;\n    }\n    final children = <Widget>[\n      const SizedBox(height: 5),\n      Row(\n        mainAxisAlignment: MainAxisAlignment.spaceAround,\n        children: [\n          const SizedBox(width: 15),\n          Text(_wrapNodeName(item), style: UIs.text13Bold),\n          Text('  /  ${item.summary}', style: UIs.text12Grey),\n          const Spacer(),\n          _buildCtrlBtns(item),\n          UIs.width13,\n        ],\n      ),\n      UIs.height7,\n      AvgSize(\n        totalSize: _media.size.width,\n        padding: _kHorziPadding * 2 + 26,\n        children: [\n          PercentCircle(percent: (item.cpu / item.maxcpu) * 100),\n          PercentCircle(percent: (item.mem / item.maxmem) * 100),\n          Column(\n            mainAxisSize: MainAxisSize.min,\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              Text(\n                '${l10n.read}:\\n${item.diskread.bytes2Str}',\n                style: UIs.text11Grey,\n                textAlign: TextAlign.center,\n              ),\n              const SizedBox(height: 3),\n              Text(\n                '${l10n.write}:\\n${item.diskwrite.bytes2Str}',\n                style: UIs.text11Grey,\n                textAlign: TextAlign.center,\n              ),\n            ],\n          ),\n          Column(\n            mainAxisSize: MainAxisSize.min,\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              Text('↓:\\n${item.netin.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),\n              const SizedBox(height: 3),\n              Text('↑:\\n${item.netout.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),\n            ],\n          ),\n        ],\n      ),\n      const SizedBox(height: 21),\n    ];\n    return Column(mainAxisSize: MainAxisSize.min, children: children).cardx;\n  }\n\n  Widget _buildLxc(PveLxc item) {\n    if (!item.available) {\n      return ListTile(\n        title: Text(_wrapNodeName(item), style: UIs.text13Bold),\n        trailing: _buildCtrlBtns(item),\n      ).cardx;\n    }\n    final children = <Widget>[\n      const SizedBox(height: 5),\n      Row(\n        mainAxisAlignment: MainAxisAlignment.spaceAround,\n        children: [\n          const SizedBox(width: 15),\n          Text(_wrapNodeName(item), style: UIs.text13Bold),\n          Text('  /  ${item.summary}', style: UIs.text12Grey),\n          const Spacer(),\n          _buildCtrlBtns(item),\n          UIs.width13,\n        ],\n      ),\n      UIs.height7,\n      AvgSize(\n        totalSize: _media.size.width,\n        padding: _kHorziPadding * 2 + 26,\n        children: [\n          PercentCircle(percent: (item.cpu / item.maxcpu) * 100),\n          PercentCircle(percent: (item.mem / item.maxmem) * 100),\n          Column(\n            mainAxisSize: MainAxisSize.min,\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              Text(\n                '${l10n.read}:\\n${item.diskread.bytes2Str}',\n                style: UIs.text11Grey,\n                textAlign: TextAlign.center,\n              ),\n              const SizedBox(height: 3),\n              Text(\n                '${l10n.write}:\\n${item.diskwrite.bytes2Str}',\n                style: UIs.text11Grey,\n                textAlign: TextAlign.center,\n              ),\n            ],\n          ),\n          Column(\n            mainAxisSize: MainAxisSize.min,\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              Text('↓:\\n${item.netin.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),\n              const SizedBox(height: 3),\n              Text('↑:\\n${item.netout.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),\n            ],\n          ),\n        ],\n      ),\n      const SizedBox(height: 21),\n    ];\n    return Column(mainAxisSize: MainAxisSize.min, children: children).cardx;\n  }\n\n  Widget _buildStorage(PveStorage item) {\n    return Padding(\n      padding: const EdgeInsets.all(13),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Row(\n            children: [\n              Text(_wrapNodeName(item), style: UIs.text13Bold),\n              const Spacer(),\n              Text(item.summary, style: UIs.text11Grey),\n            ],\n          ),\n          UIs.height7,\n          KvRow(k: libL10n.content, v: item.content),\n          KvRow(k: l10n.plugInType, v: item.plugintype),\n        ],\n      ),\n    ).cardx;\n  }\n\n  Widget _buildSdn(PveSdn item) {\n    return ListTile(title: Text(_wrapNodeName(item)), trailing: Text(item.summary)).cardx;\n  }\n\n  Widget _buildCtrlBtns(PveCtrlIface item) {\n    const pad = EdgeInsets.symmetric(horizontal: 7, vertical: 5);\n    if (!item.available) {\n      return Btn.icon(\n        icon: const Icon(Icons.play_arrow, color: Colors.grey),\n        onTap: () => _onCtrl(libL10n.start, item, () => _notifier.start(item.node, item.id)),\n      );\n    }\n    return Row(\n      children: [\n        Btn.icon(\n          icon: const Icon(Icons.stop, color: Colors.grey, size: 20),\n          padding: pad,\n          onTap: () => _onCtrl(libL10n.stop, item, () => _notifier.stop(item.node, item.id)),\n        ),\n        Btn.icon(\n          icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),\n          padding: pad,\n          onTap: () => _onCtrl(libL10n.reboot, item, () => _notifier.reboot(item.node, item.id)),\n        ),\n        Btn.icon(\n          icon: const Icon(Icons.power_off, color: Colors.grey, size: 20),\n          padding: pad,\n          onTap: () => _onCtrl(libL10n.shutdown, item, () => _notifier.shutdown(item.node, item.id)),\n        ),\n      ],\n    );\n  }\n}\n\nextension on _PvePageState {\n  void _onCtrl(String action, PveCtrlIface item, Future<bool> Function() func) async {\n    final sure = await context.showRoundDialog<bool>(\n      title: libL10n.attention,\n      child: Text(libL10n.askContinue('$action ${item.id}')),\n      actions: Btnx.okReds,\n    );\n    if (sure != true) return;\n\n    final (suc, err) = await context.showLoadingDialog(fn: func);\n    if (suc == true) {\n      context.showSnackBar(libL10n.success);\n    } else {\n      context.showSnackBar(err?.toString() ?? libL10n.fail);\n    }\n  }\n\n  /// Add PveNode if only one node exists\n  String _wrapNodeName(PveCtrlIface item) {\n    final pveState = ref.read(_provider);\n    if (pveState.data?.onlyOneNode ?? false) {\n      return item.name;\n    }\n    return '${item.node} / ${item.name}';\n  }\n\n  void _initRefreshTimer() {\n    _timer?.cancel();\n    _timer = Timer.periodic(Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()), (_) {\n      if (mounted) {\n        _notifier.list();\n      }\n    });\n  }\n\n  void _afterInit() async {\n    // Wait for the PVE state to be connected\n    while (mounted) {\n      final pveState = ref.read(_provider);\n      if (pveState.isConnected) {\n        if (pveState.release != null && pveState.release!.compareTo('8.0') < 0) {\n          if (mounted) {\n            context.showSnackBar(l10n.pveVersionLow);\n          }\n        }\n        break;\n      }\n      if (pveState.error != null) {\n        break; // Skip if there is an error\n      }\n      await Future.delayed(const Duration(milliseconds: 100));\n    }\n  }\n}\n"
  },
  {
    "path": "lib/view/page/server/detail/misc.dart",
    "content": "part of 'view.dart';\n\nextension on _ServerDetailPageState {\n  void _onTapNvidiaGpuItem(NvidiaSmiItem item) {\n    final processes = item.memory.processes;\n    final displayCount = processes.length > 5 ? 5 : processes.length;\n    final height = displayCount * 47.0;\n    context.showRoundDialog(\n      title: item.name,\n      child: SizedBox(\n        width: double.maxFinite,\n        height: height,\n        child: ListView.builder(\n          itemCount: processes.length,\n          itemBuilder: (_, idx) => _buildGpuProcessItem(processes[idx]),\n        ),\n      ),\n      actions: Btnx.oks,\n    );\n  }\n\n  void _onTapAmdGpuItem(AmdSmiItem item) {\n    final processes = item.memory.processes;\n    final displayCount = processes.length > 5 ? 5 : processes.length;\n    final height = displayCount * 47.0;\n    context.showRoundDialog(\n      title: item.name,\n      child: SizedBox(\n        width: double.maxFinite,\n        height: height,\n        child: ListView.builder(\n          itemCount: processes.length,\n          itemBuilder: (_, idx) => _buildAmdGpuProcessItem(processes[idx]),\n        ),\n      ),\n      actions: Btnx.oks,\n    );\n  }\n\n  void _onTapGpuProcessItem(NvidiaSmiMemProcess process) {\n    context.showRoundDialog(\n      title: '${process.pid}',\n      titleMaxLines: 1,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          UIs.height13,\n          Text('Memory: ${process.memory} MiB'),\n          UIs.height13,\n          Text('Process: ${process.name}'),\n        ],\n      ),\n      actions: [TextButton(onPressed: () => context.pop(), child: Text(libL10n.close))],\n    );\n  }\n\n  void _onTapAmdGpuProcessItem(AmdSmiMemProcess process) {\n    context.showRoundDialog(\n      title: '${process.pid}',\n      titleMaxLines: 1,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          UIs.height13,\n          Text('Memory: ${process.memory} ${process.memory > 1024 ? 'MB' : 'KB'}'),\n          UIs.height13,\n          Text('Process: ${process.name}'),\n        ],\n      ),\n      actions: [TextButton(onPressed: () => context.pop(), child: Text(libL10n.close))],\n    );\n  }\n\n  void _onTapCustomItem(MapEntry<String, String> cmd) {\n    context.showRoundDialog(\n      title: cmd.key,\n      child: SingleChildScrollView(child: Text(cmd.value, style: UIs.text13Grey)),\n      actions: [TextButton(onPressed: () => context.pop(), child: Text(libL10n.close))],\n    );\n  }\n\n  void _onTapSensorItem(SensorItem si) {\n    context.showRoundDialog(\n      title: si.device,\n      child: SingleChildScrollView(\n        child: SimpleMarkdown(\n          data: si.toMarkdown,\n          styleSheet: MarkdownStyleSheet(\n            tableBorder: TableBorder.all(color: Colors.grey),\n            tableHead: const TextStyle(fontWeight: FontWeight.bold),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _onTapTemperatureItem(String key) {\n    Pfs.copy(key);\n    context.showSnackBar('${libL10n.copy} ${libL10n.success}');\n  }\n\n  bool _getInitExpand(int len, [int? max]) {\n    if (!_collapse) return true;\n    if (_size.width > UIs.columnWidth) return true;\n    return len > 0 && len <= (max ?? 3);\n  }\n}\n\nenum _NetSortType {\n  device,\n  trans,\n  recv;\n\n  bool get isDevice => this == _NetSortType.device;\n  bool get isIn => this == _NetSortType.recv;\n  bool get isOut => this == _NetSortType.trans;\n\n  _NetSortType get next {\n    switch (this) {\n      case device:\n        return trans;\n      case _NetSortType.trans:\n        return recv;\n      case recv:\n        return device;\n    }\n  }\n\n  int Function(String, String) getSortFunc(NetSpeed ns) {\n    switch (this) {\n      case _NetSortType.device:\n        return (b, a) => a.compareTo(b);\n      case _NetSortType.recv:\n        return (b, a) => ns.speedInBytes(ns.deviceIdx(a)).compareTo(ns.speedInBytes(ns.deviceIdx(b)));\n      case _NetSortType.trans:\n        return (b, a) => ns.speedOutBytes(ns.deviceIdx(a)).compareTo(ns.speedOutBytes(ns.deviceIdx(b)));\n    }\n  }\n}\n\nWidget _buildLineChart(\n  List<List<FlSpot>> spots, {\n  String? tooltipPrefix,\n  bool curve = false,\n  int verticalInterval = 20,\n}) {\n  return LineChart(\n    LineChartData(\n      lineTouchData: LineTouchData(\n        touchTooltipData: LineTouchTooltipData(\n          tooltipPadding: const EdgeInsets.all(5),\n          tooltipBorderRadius: BorderRadius.circular(8),\n          getTooltipItems: (List<LineBarSpot> touchedSpots) {\n            return touchedSpots.map((e) {\n              return LineTooltipItem(\n                '$tooltipPrefix${e.barIndex}: ${e.y.toStringAsFixed(2)}',\n                const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),\n              );\n            }).toList();\n          },\n        ),\n        handleBuiltInTouches: true,\n      ),\n      gridData: FlGridData(\n        show: true,\n        drawVerticalLine: false,\n        horizontalInterval: verticalInterval.toDouble(),\n        getDrawingHorizontalLine: (value) {\n          return const FlLine(color: Color.fromARGB(43, 88, 91, 94), strokeWidth: 1);\n        },\n      ),\n      titlesData: FlTitlesData(\n        show: true,\n        rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),\n        topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),\n        bottomTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),\n        leftTitles: AxisTitles(\n          sideTitles: SideTitles(\n            showTitles: true,\n            interval: 20,\n            getTitlesWidget: (val, meta) {\n              if (val % verticalInterval != 0) return UIs.placeholder;\n              if (val == 0) return const Text('0 %', style: UIs.text12Grey);\n              return Text(val.toInt().toString(), style: UIs.text12Grey);\n            },\n            reservedSize: 27,\n          ),\n        ),\n      ),\n      borderData: FlBorderData(show: false),\n      minY: -1,\n      maxY: 101,\n      lineBarsData: spots\n          .map(\n            (e) => LineChartBarData(\n              spots: e,\n              isCurved: curve,\n              barWidth: 2,\n              isStrokeCapRound: true,\n              color: UIs.primaryColor,\n              dotData: const FlDotData(show: false),\n              belowBarData: BarAreaData(show: false),\n            ),\n          )\n          .toList(),\n    ),\n  );\n}\n"
  },
  {
    "path": "lib/view/page/server/detail/view.dart",
    "content": "import 'package:extended_image/extended_image.dart';\nimport 'package:fl_chart/fl_chart.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_markdown_plus/flutter_markdown_plus.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/extension/server.dart';\nimport 'package:server_box/core/route.dart';\nimport 'package:server_box/data/model/app/scripts/cmd_types.dart';\nimport 'package:server_box/data/model/app/server_detail_card.dart';\nimport 'package:server_box/data/model/server/amd.dart';\nimport 'package:server_box/data/model/server/battery.dart';\nimport 'package:server_box/data/model/server/cpu.dart';\nimport 'package:server_box/data/model/server/disk.dart';\nimport 'package:server_box/data/model/server/disk_smart.dart';\nimport 'package:server_box/data/model/server/net_speed.dart';\nimport 'package:server_box/data/model/server/nvdia.dart';\nimport 'package:server_box/data/model/server/sensors.dart';\nimport 'package:server_box/data/model/server/server.dart' as server_model;\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/system.dart';\nimport 'package:server_box/data/provider/server/single.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/view/page/pve.dart';\nimport 'package:server_box/view/page/server/edit/edit.dart';\nimport 'package:server_box/view/widget/server_func_btns.dart';\n\npart 'misc.dart';\n\nclass ServerDetailPage extends ConsumerStatefulWidget {\n  final SpiRequiredArgs args;\n  const ServerDetailPage({super.key, required this.args});\n\n  @override\n  ConsumerState<ServerDetailPage> createState() => _ServerDetailPageState();\n\n  static const route = AppRouteArg(page: ServerDetailPage.new, path: '/servers/detail');\n}\n\nclass _ServerDetailPageState extends ConsumerState<ServerDetailPage> with SingleTickerProviderStateMixin {\n  late final _cardBuildMap = Map.fromIterables(ServerDetailCards.names, [\n    _buildAbout,\n    _buildCPUView,\n    _buildMemView,\n    _buildSwapView,\n    _buildGpuView,\n    _buildDiskView,\n    _buildDiskSmart,\n    _buildNetView,\n    _buildSensors,\n    _buildTemperature,\n    _buildBatteries,\n    _buildPve,\n    _buildCustomCmd,\n  ]);\n\n  late Size _size;\n  final List<String> _cardsOrder = [];\n\n  final _settings = Stores.setting;\n  final _netSortType = ValueNotifier(_NetSortType.device);\n  late final _collapse = _settings.collapseUIDefault.fetch();\n  late final _textFactor = TextScaler.linear(_settings.textFactor.fetch());\n  late final _cpuViewAsProgress = _settings.cpuViewAsProgress.fetch();\n  late final _moveServerFuncs = _settings.moveServerFuncs.fetch();\n  late final _displayCpuIndex = _settings.displayCpuIndex.fetch();\n\n  @override\n  void dispose() {\n    super.dispose();\n    _netSortType.dispose();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    _size = MediaQuery.sizeOf(context);\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    final order = _settings.detailCardOrder.fetch();\n    final disabled = _settings.detailCardDisabled.fetch();\n    order.removeWhere((e) => !ServerDetailCards.names.contains(e) || disabled.contains(e));\n    _cardsOrder.addAll(order);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final serverState = ref.watch(serverProvider(widget.args.spi.id));\n    if (serverState.client == null) {\n      return Scaffold(\n        appBar: CustomAppBar(),\n        body: Center(child: Text(libL10n.empty)),\n      );\n    }\n    return _buildMainPage(serverState);\n  }\n\n  Widget _buildMainPage(ServerState si) {\n    final buildFuncs = !_moveServerFuncs;\n    final logo = _buildLogo(si);\n    final children = <Widget>[?logo, if (buildFuncs) ServerFuncBtns(spi: si.spi)];\n    for (final card in _cardsOrder) {\n      final child = _cardBuildMap[card]?.call(si);\n      if (child != null) {\n        children.add(child);\n      }\n    }\n\n    return Scaffold(\n      appBar: _buildAppBar(si),\n      body: SafeArea(child: AutoMultiList(children: children)),\n    );\n  }\n\n  CustomAppBar _buildAppBar(ServerState si) {\n    return CustomAppBar(\n      title: Text(\n        si.spi.name,\n        style: TextStyle(fontSize: 20, color: context.isDark ? Colors.white : Colors.black),\n      ),\n      actions: [\n        QrShareBtn(data: si.spi.toJsonString(), tip: si.spi.name, tip2: '${libL10n.server} ~ ServerBox'),\n        IconButton(\n          icon: const Icon(Icons.edit),\n          onPressed: () async {\n            final delete = await ServerEditPage.route.go(context, args: SpiRequiredArgs(si.spi));\n            if (delete == true) {\n              context.pop();\n            }\n          },\n        ),\n      ],\n    );\n  }\n\n  Widget? _buildLogo(ServerState si) {\n    final logoUrl = si.getLogoUrl(context);\n\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 13),\n      child: LayoutBuilder(\n        builder: (_, cons) {\n          if (logoUrl == null) {\n            return UIs.placeholder;\n          }\n          return ExtendedImage.network(\n            logoUrl,\n            cache: true,\n            height: cons.maxWidth * 0.3,\n            width: cons.maxWidth,\n          );\n        },\n      ),\n    );\n  }\n\n  Widget? _buildAbout(ServerState si) {\n    final ss = si.status;\n    return ExpandTile(\n      key: ValueKey(ss.more.hashCode), // Use hashCode to avoid perf issue\n      leading: const Icon(MingCute.information_fill, size: 20),\n      initiallyExpanded: _getInitExpand(ss.more.entries.length),\n      title: Text(libL10n.about),\n      childrenPadding: const EdgeInsets.symmetric(horizontal: 17, vertical: 11),\n      children: ss.more.entries\n          .map(\n            (e) => Padding(\n              padding: const EdgeInsets.symmetric(vertical: 2),\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                children: [\n                  Text(e.key.i18n, style: UIs.text13, overflow: TextOverflow.ellipsis),\n                  Text(e.value, style: UIs.text13Grey, overflow: TextOverflow.ellipsis),\n                ],\n              ),\n            ),\n          )\n          .toList(),\n    ).cardx;\n  }\n\n  Widget? _buildCPUView(ServerState si) {\n    final ss = si.status;\n    final percent = ss.cpu.usedPercent(coreIdx: 0).toInt();\n    final details = [\n      _buildDetailPercent(ss.cpu.user, 'user'),\n      UIs.width13,\n      _buildDetailPercent(ss.cpu.idle, 'idle'),\n    ];\n    if (ss.system == SystemType.linux) {\n      details.addAll([\n        UIs.width13,\n        _buildDetailPercent(ss.cpu.sys, 'sys'),\n        UIs.width13,\n        _buildDetailPercent(ss.cpu.iowait, 'io'),\n      ]);\n    }\n\n    final List<Widget> children = _cpuViewAsProgress\n        ? _buildCPUProgress(ss.cpu)\n        : [_buildCPUChart(ss)];\n\n    if (ss.cpu.brand.isNotEmpty) {\n      children.add(\n        Column(children: ss.cpu.brand.entries.map(_buildCpuModelItem).toList()).paddingOnly(top: 13),\n      );\n    }\n\n    return ExpandTile(\n      title: Align(\n        alignment: Alignment.centerLeft,\n        child: _buildAnimatedText(ValueKey(percent), '$percent%', UIs.text27),\n      ),\n      childrenPadding: const EdgeInsets.symmetric(vertical: 13),\n      initiallyExpanded: _getInitExpand(1),\n      trailing: Row(mainAxisSize: MainAxisSize.min, children: details),\n      children: children,\n    ).cardx;\n  }\n\n  Widget _buildCpuModelItem(MapEntry<String, int> e) {\n    final name = e.key\n        .replaceFirst('Intel(R)', '')\n        .replaceFirst('AMD', '')\n        .replaceFirst('with Radeon Graphics', '');\n    final child = Row(\n      mainAxisAlignment: MainAxisAlignment.spaceBetween,\n      children: [\n        LayoutBuilder(\n          builder: (_, cons) {\n            return ConstrainedBox(\n              constraints: BoxConstraints(maxWidth: cons.maxWidth * .7),\n              child: Text(name, style: UIs.text13, overflow: TextOverflow.ellipsis, maxLines: 1),\n            );\n          },\n        ),\n        Text('x ${e.value}', style: UIs.text13Grey, overflow: TextOverflow.clip),\n      ],\n    );\n    return child.paddingSymmetric(horizontal: 17);\n  }\n\n  Widget _buildDetailPercent(double percent, String timeType) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      mainAxisAlignment: MainAxisAlignment.center,\n      crossAxisAlignment: CrossAxisAlignment.end,\n      children: [\n        Text('${percent.toStringAsFixed(1)}%', style: UIs.text12, textScaler: _textFactor),\n        Text(timeType, style: UIs.text12Grey, textScaler: _textFactor),\n      ],\n    );\n  }\n\n  List<Widget> _buildCPUProgress(Cpus cs) {\n    const kMaxColumn = 2;\n    const kRowThreshold = 4;\n    const kCoresCountThreshold = kMaxColumn * kRowThreshold;\n    final children = <Widget>[];\n    final displayCpuIndexSetting = _displayCpuIndex;\n\n    if (cs.coresCount > kCoresCountThreshold) {\n      final numCoresToDisplay = cs.coresCount - 1;\n      final numRows = (numCoresToDisplay + kMaxColumn - 1) ~/ kMaxColumn;\n\n      for (var i = 0; i < numRows; i++) {\n        final rowChildren = <Widget>[];\n        for (var j = 0; j < kMaxColumn; j++) {\n          final coreListIndex = i * kMaxColumn + j;\n          if (coreListIndex >= numCoresToDisplay) break;\n\n          final coreNumberOneBased = coreListIndex + 1;\n\n          if (displayCpuIndexSetting) {\n            rowChildren.add(Text('$coreNumberOneBased', style: UIs.text13Grey));\n          }\n          rowChildren.add(\n            Expanded(\n              child: Padding(\n                padding: const EdgeInsets.symmetric(vertical: 3),\n                child: _buildProgress(cs.usedPercent(coreIdx: coreNumberOneBased)),\n              ),\n            ),\n          );\n        }\n        if (rowChildren.isNotEmpty) {\n          children.add(\n            Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 17),\n              child: Row(children: rowChildren.joinWith(UIs.width7).toList()),\n            ),\n          );\n        }\n      }\n    } else {\n      for (var i = 1; i < cs.coresCount; i++) {\n        children.add(\n          Padding(\n            padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 17),\n            child: _buildProgress(cs.usedPercent(coreIdx: i)),\n          ),\n        );\n      }\n    }\n\n    return children;\n  }\n\n  Widget _buildCPUChart(server_model.ServerStatus ss) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 13),\n      child: LayoutBuilder(\n        builder: (_, cons) {\n          return SizedBox(\n            height: 137,\n            width: cons.maxWidth,\n            child: _buildLineChart(\n              ss.cpu.spots,\n              //ss.cpu.rangeX,\n              tooltipPrefix: 'CPU',\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildProgress(double percent) {\n    if (percent > 100) percent = 100;\n    final percentWithinOne = percent / 100;\n    return LinearProgressIndicator(\n      value: percentWithinOne,\n      minHeight: 7,\n      backgroundColor: UIs.halfAlpha,\n      color: UIs.primaryColor,\n    );\n  }\n\n  Widget? _buildMemView(ServerState si) {\n    final ss = si.status;\n    final free = ss.mem.free / ss.mem.total * 100;\n    final avail = ss.mem.availPercent * 100;\n    final used = ss.mem.usedPercent * 100;\n    final usedStr = used.toStringAsFixed(0);\n\n    final percentW = Row(\n      children: [\n        _buildAnimatedText(ValueKey(usedStr), '$usedStr%', UIs.text27),\n        UIs.width7,\n        Text('of ${(ss.mem.total * 1024).bytes2Str}', style: UIs.text13Grey),\n      ],\n    );\n\n    return Padding(\n      padding: UIs.roundRectCardPadding,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.center,\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          Row(\n            mainAxisAlignment: MainAxisAlignment.spaceBetween,\n            children: [\n              percentW,\n              Row(\n                children: [\n                  _buildDetailPercent(free, 'free'),\n                  UIs.width13,\n                  _buildDetailPercent(avail, 'avail'),\n                ],\n              ),\n            ],\n          ),\n          UIs.height13,\n          _buildProgress(used),\n        ],\n      ),\n    ).cardx;\n  }\n\n  Widget? _buildSwapView(ServerState si) {\n    final ss = si.status;\n    if (ss.swap.total == 0) return null;\n\n    final used = ss.swap.usedPercent * 100;\n    final cached = ss.swap.cached / ss.swap.total * 100;\n\n    final percentW = Row(\n      children: [\n        Text('${used.toStringAsFixed(0)}%', style: UIs.text27),\n        UIs.width7,\n        Text('of ${(ss.swap.total * 1024).bytes2Str} ', style: UIs.text13Grey),\n      ],\n    );\n\n    return Padding(\n      padding: UIs.roundRectCardPadding,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.center,\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          Row(\n            mainAxisAlignment: MainAxisAlignment.spaceBetween,\n            children: [percentW, _buildDetailPercent(cached, 'cached')],\n          ),\n          UIs.height13,\n          _buildProgress(used),\n        ],\n      ),\n    ).cardx;\n  }\n\n  Widget? _buildGpuView(ServerState si) {\n    final ss = si.status;\n    final hasNvidia = ss.nvidia != null && ss.nvidia!.isNotEmpty;\n    final hasAmd = ss.amd != null && ss.amd!.isNotEmpty;\n\n    if (!hasNvidia && !hasAmd) return null;\n\n    final children = <Widget>[];\n\n    // Add NVIDIA GPUs\n    if (hasNvidia) {\n      children.addAll(ss.nvidia!.map((e) => _buildNvidiaGpuItem(e)));\n    }\n\n    // Add AMD GPUs\n    if (hasAmd) {\n      children.addAll(ss.amd!.map((e) => _buildAmdGpuItem(e)));\n    }\n\n    return ExpandTile(\n      title: const Text('GPU'),\n      leading: const Icon(Icons.memory, size: 17),\n      initiallyExpanded: _getInitExpand(children.length, 3),\n      children: children,\n    ).cardx;\n  }\n\n  Widget _buildNvidiaGpuItem(NvidiaSmiItem item) {\n    final mem = item.memory;\n    return ListTile(\n      title: Text(item.name, style: UIs.text13),\n      leading: Text(\n        '${item.percent}%\\n${item.temp} °C',\n        style: UIs.text12Grey,\n        textScaler: _textFactor,\n        textAlign: TextAlign.center,\n      ),\n      subtitle: Text(\n        '${item.power} - FAN ${item.fanSpeed}%\\n${mem.used} / ${mem.total} ${mem.unit}',\n        style: UIs.text12Grey,\n        textScaler: _textFactor,\n      ),\n      contentPadding: const EdgeInsets.only(left: 17, right: 17),\n      trailing: Row(\n        mainAxisAlignment: MainAxisAlignment.end,\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          IconButton(\n            onPressed: () => _onTapNvidiaGpuItem(item),\n            icon: const Icon(Icons.info_outline, size: 17),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildAmdGpuItem(AmdSmiItem item) {\n    final mem = item.memory;\n    return ListTile(\n      title: Text('${item.name} (AMD)', style: UIs.text13),\n      leading: Text(\n        '${item.utilization}%\\n${item.temp} °C',\n        style: UIs.text12Grey,\n        textScaler: _textFactor,\n        textAlign: TextAlign.center,\n      ),\n      subtitle: Text(\n        '${item.power} - FAN ${item.fanSpeed} RPM\\n${item.clockSpeed} MHz\\n${mem.used} / ${mem.total} ${mem.unit}',\n        style: UIs.text12Grey,\n        textScaler: _textFactor,\n      ),\n      contentPadding: const EdgeInsets.only(left: 17, right: 17),\n      trailing: Row(\n        mainAxisAlignment: MainAxisAlignment.end,\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          IconButton(onPressed: () => _onTapAmdGpuItem(item), icon: const Icon(Icons.info_outline, size: 17)),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildGpuProcessItem(NvidiaSmiMemProcess process) {\n    return ListTile(\n      title: Text(\n        process.name,\n        style: UIs.text12,\n        maxLines: 1,\n        overflow: TextOverflow.ellipsis,\n        textScaler: _textFactor,\n      ),\n      subtitle: Text(\n        'PID: ${process.pid} - ${process.memory} MiB',\n        style: UIs.text12Grey,\n        textScaler: _textFactor,\n      ),\n      trailing: InkWell(\n        onTap: () => _onTapGpuProcessItem(process),\n        child: const Icon(Icons.info_outline, size: 17),\n      ),\n    );\n  }\n\n  Widget _buildAmdGpuProcessItem(AmdSmiMemProcess process) {\n    return ListTile(\n      title: Text(\n        process.name,\n        style: UIs.text12,\n        maxLines: 1,\n        overflow: TextOverflow.ellipsis,\n        textScaler: _textFactor,\n      ),\n      subtitle: Text(\n        'PID: ${process.pid} - ${process.memory} MiB',\n        style: UIs.text12Grey,\n        textScaler: _textFactor,\n      ),\n      trailing: InkWell(\n        onTap: () => _onTapAmdGpuProcessItem(process),\n        child: const Icon(Icons.info_outline, size: 17),\n      ),\n    );\n  }\n\n  Widget? _buildDiskView(ServerState si) {\n    final ss = si.status;\n    final children = <Widget>[];\n\n    // Create widgets for each top-level disk\n    for (int idx = 0; idx < ss.disk.length; idx++) {\n      final disk = ss.disk[idx];\n      children.add(_buildDiskItemWithHierarchy(disk, ss, 0));\n    }\n\n    if (children.isEmpty) return null;\n\n    return ExpandTile(\n      title: Text(libL10n.disk),\n      childrenPadding: const EdgeInsets.only(bottom: 7),\n      leading: Icon(ServerDetailCards.disk.icon, size: 17),\n      initiallyExpanded: _getInitExpand(children.length),\n      children: children,\n    ).cardx;\n  }\n\n  Widget _buildDiskItemWithHierarchy(Disk disk, server_model.ServerStatus ss, int depth) {\n    // Create a list to hold this disk and its children\n    final items = <Widget>[];\n\n    // Add the current disk\n    items.add(_buildDiskItem(disk, ss, depth));\n\n    // Recursively add child disks with increased indentation\n    if (disk.children.isNotEmpty) {\n      for (final childDisk in disk.children) {\n        items.add(_buildDiskItemWithHierarchy(childDisk, ss, depth + 1));\n      }\n    }\n\n    return Column(children: items);\n  }\n\n  Widget _buildDiskItem(Disk disk, server_model.ServerStatus ss, int depth) {\n    final (read, write) = ss.diskIO.getSpeed(disk.path);\n    final text = () {\n      final use = '${l10n.used} ${disk.used.kb2Str} / ${disk.size.kb2Str}';\n      if (read == null || write == null) return use;\n      return '$use\\n${l10n.read} $read | ${l10n.write} $write';\n    }();\n\n    return Padding(\n      padding: EdgeInsets.only(\n        left: 17.0 + (depth * 15.0), // Indent based on depth\n        right: 17.0,\n        top: 5.0,\n        bottom: 5.0,\n      ),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        crossAxisAlignment: CrossAxisAlignment.center,\n        children: [\n          Expanded(\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(\n                  disk.mount.isEmpty ? disk.path : '${disk.path} (${disk.mount})',\n                  style: UIs.text12,\n                  textScaler: _textFactor,\n                ),\n                Text(text, style: UIs.text12Grey, textScaler: _textFactor),\n              ],\n            ),\n          ),\n          if (disk.size > BigInt.zero)\n            SizedBox(\n              height: 41,\n              width: 41,\n              child: Stack(\n                alignment: Alignment.center,\n                children: [\n                  CircularProgressIndicator(\n                    value: disk.usedPercent / 100,\n                    strokeWidth: 5,\n                    backgroundColor: UIs.halfAlpha,\n                    color: UIs.primaryColor,\n                  ),\n                  Text('${disk.usedPercent}%', style: UIs.text12Grey),\n                ],\n              ),\n            ),\n        ],\n      ),\n    );\n  }\n\n  Widget? _buildDiskSmart(ServerState si) {\n    final smarts = si.status.diskSmart;\n    if (smarts.isEmpty) return null;\n    return CardX(\n      child: ExpandTile(\n        title: Text(l10n.diskHealth),\n        leading: Icon(ServerDetailCards.smart.icon, size: 17),\n        childrenPadding: const EdgeInsets.only(bottom: 7),\n        initiallyExpanded: _getInitExpand(smarts.length),\n        children: smarts.map(_buildDiskSmartItem).toList(),\n      ),\n    );\n  }\n\n  Widget _buildDiskSmartItem(DiskSmart smart) {\n    final healthStatus = _getDiskHealthStatus(smart);\n\n    return ListTile(\n      dense: true,\n      leading: healthStatus.icon,\n      title: Text(smart.device, style: UIs.text13, textScaler: _textFactor),\n      trailing: Text(\n        healthStatus.text,\n        style: UIs.text13.copyWith(fontWeight: FontWeight.bold),\n        textScaler: _textFactor,\n      ),\n      subtitle: _buildDiskSmartDetails(smart),\n      onTap: () => _onTapDiskSmartItem(smart),\n    );\n  }\n\n  ({String text, Color color, Widget icon}) _getDiskHealthStatus(DiskSmart smart) {\n    if (smart.healthy == null) {\n      return (\n        text: libL10n.unknown,\n        color: Colors.orange,\n        icon: const Icon(Icons.help_outline, color: Colors.orange, size: 18),\n      );\n    } else if (smart.healthy!) {\n      return (\n        text: 'PASS',\n        color: Colors.green,\n        icon: const Icon(Icons.check_circle, color: Colors.green, size: 18),\n      );\n    } else {\n      return (text: 'FAIL', color: Colors.red, icon: const Icon(Icons.error, color: Colors.red, size: 18));\n    }\n  }\n\n  Widget? _buildDiskSmartDetails(DiskSmart smart) {\n    final details = <String>[];\n\n    if (smart.model != null) {\n      details.add(smart.model!);\n    }\n\n    if (smart.temperature != null) {\n      details.add('${smart.temperature!.toStringAsFixed(1)}°C');\n    }\n\n    if (smart.powerOnHours != null) {\n      final hours = smart.powerOnHours!;\n      details.add('$hours ${libL10n.hour}');\n    }\n\n    if (smart.ssdLifeLeft != null) {\n      details.add('Life left: ${smart.ssdLifeLeft}%');\n    }\n\n    if (details.isEmpty) return null;\n\n    return Text(\n      details.join(' | '),\n      style: UIs.text12Grey,\n      textScaler: _textFactor,\n      maxLines: 2,\n      overflow: TextOverflow.ellipsis,\n    );\n  }\n\n  void _onTapDiskSmartItem(DiskSmart smart) {\n    final details = <String>[];\n\n    if (smart.model != null) details.add('Model: ${smart.model}');\n    if (smart.serial != null) details.add('Serial: ${smart.serial}');\n    if (smart.temperature != null) {\n      details.add('Temperature: ${smart.temperature!.toStringAsFixed(1)}°C');\n    }\n\n    if (smart.powerOnHours != null) {\n      details.add('Power On: ${smart.powerOnHours} ${libL10n.hour}');\n    }\n    if (smart.powerCycleCount != null) {\n      details.add('Power Cycle: ${smart.powerCycleCount}');\n    }\n\n    if (smart.ssdLifeLeft != null) {\n      details.add('Life Left: ${smart.ssdLifeLeft}%');\n    }\n    if (smart.lifetimeWritesGiB != null) {\n      details.add('Lifetime Write: ${smart.lifetimeWritesGiB} GiB');\n    }\n    if (smart.lifetimeReadsGiB != null) {\n      details.add('Lifetime Read: ${smart.lifetimeReadsGiB} GiB');\n    }\n    if (smart.averageEraseCount != null) {\n      details.add('Avg. Erase: ${smart.averageEraseCount}');\n    }\n    if (smart.unsafeShutdownCount != null) {\n      details.add('Unsafe Shutdown: ${smart.unsafeShutdownCount}');\n    }\n\n    final criticalAttrs = [\n      'Reallocated_Sector_Ct',\n      'Current_Pending_Sector',\n      'Offline_Uncorrectable',\n      'UDMA_CRC_Error_Count',\n    ];\n\n    for (final attrName in criticalAttrs) {\n      final attr = smart.getAttribute(attrName);\n      if (attr != null && attr.rawValue != null) {\n        final value = attr.rawValue.toString();\n        details.add('${attrName.replaceAll('_', ' ')}: $value');\n      }\n    }\n\n    if (details.isEmpty) {\n      return;\n    }\n\n    final markdown = details.join('\\n\\n- ');\n    context.showRoundDialog(\n      title: smart.device,\n      child: MarkdownBody(\n        data: '- $markdown',\n        selectable: true,\n        styleSheet: MarkdownStyleSheet.fromTheme(\n          Theme.of(context),\n        ).copyWith(p: UIs.text13Grey, h2: UIs.text15),\n      ),\n      actions: Btnx.oks,\n    );\n  }\n\n  Widget? _buildNetView(ServerState si) {\n    final ss = si.status;\n    final ns = ss.netSpeed;\n    final children = <Widget>[];\n    final devices = ns.devices;\n    if (devices.isEmpty) return null;\n\n    devices.sort(_netSortType.value.getSortFunc(ns));\n    children.addAll(devices.map((e) => _buildNetSpeedItem(ns, e)));\n\n    return ExpandTile(\n      leading: Icon(ServerDetailCards.net.icon, size: 17),\n      title: Row(\n        children: [\n          Text(libL10n.net),\n          UIs.width13,\n          _netSortType.listenVal(\n            (val) => InkWell(\n              onTap: () => _netSortType.value = val.next,\n              child: AnimatedSwitcher(\n                duration: const Duration(milliseconds: 377),\n                transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child),\n                child: Row(\n                  children: [\n                    const Icon(Icons.sort, size: 17),\n                    UIs.width7,\n                    Text(val.name, style: UIs.text13Grey),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n      childrenPadding: const EdgeInsets.only(bottom: 11),\n      initiallyExpanded: _getInitExpand(children.length),\n      children: children,\n    ).cardx;\n  }\n\n  Widget _buildNetSpeedItem(NetSpeed ns, String device) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Text(\n                device,\n                style: UIs.text12,\n                textScaler: _textFactor,\n                maxLines: 1,\n                overflow: TextOverflow.fade,\n                textAlign: TextAlign.left,\n              ),\n              Text(\n                '${ns.sizeIn(device: device)} | ${ns.sizeOut(device: device)}',\n                style: UIs.text12Grey,\n                textScaler: _textFactor,\n              ),\n            ],\n          ),\n          SizedBox(\n            width: 170,\n            child: Text(\n              '${ns.speedOut(device: device)} ↑\\n${ns.speedIn(device: device)} ↓',\n              textAlign: TextAlign.end,\n              style: UIs.text13Grey,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget? _buildTemperature(ServerState si) {\n    final ss = si.status;\n    if (ss.temps.isEmpty) return null;\n\n    return CardX(\n      child: ExpandTile(\n        title: Text(libL10n.temperature),\n        leading: const Icon(Icons.ac_unit, size: 20),\n        initiallyExpanded: _getInitExpand(ss.temps.devices.length),\n        childrenPadding: const EdgeInsets.only(bottom: 7),\n        children: ss.temps.devices.map((key) => _buildTemperatureItem(key, ss.temps.get(key))).toList(),\n      ),\n    );\n  }\n\n  Widget _buildTemperatureItem(String key, double? val) {\n    return Padding(\n      padding: const EdgeInsets.only(left: 3, right: 17, top: 5, bottom: 5),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          Btn.text(\n            text: key,\n            textStyle: UIs.text15,\n            onTap: () => _onTapTemperatureItem(key),\n          ).paddingSymmetric(horizontal: 5),\n          Text('${val?.toStringAsFixed(1)}°C', style: UIs.text13Grey),\n        ],\n      ),\n    );\n  }\n\n  Widget? _buildBatteries(ServerState si) {\n    final ss = si.status;\n    if (ss.batteries.isEmpty) return null;\n\n    return CardX(\n      child: ExpandTile(\n        title: Text(libL10n.battery),\n        leading: const Icon(Icons.battery_charging_full, size: 17),\n        childrenPadding: const EdgeInsets.only(bottom: 7),\n        initiallyExpanded: _getInitExpand(ss.batteries.length, 2),\n        children: ss.batteries.map(_buildBatteryItem).toList(),\n      ),\n    );\n  }\n\n  Widget _buildBatteryItem(Battery battery) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 5),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Text('${battery.name}', style: UIs.text15),\n              Text('${battery.status.name} - ${battery.cycle}', style: UIs.text13Grey),\n            ],\n          ),\n          Text('${battery.percent?.toStringAsFixed(0)}%', style: UIs.text13Grey),\n        ],\n      ),\n    );\n  }\n\n  Widget? _buildSensors(ServerState si) {\n    final ss = si.status;\n    if (ss.sensors.isEmpty) return UIs.placeholder;\n    return CardX(\n      child: ExpandTile(\n        title: Text(libL10n.sensors),\n        leading: const Icon(Icons.thermostat, size: 17),\n        childrenPadding: const EdgeInsets.only(bottom: 7),\n        initiallyExpanded: _getInitExpand(ss.sensors.length, 2),\n        children: ss.sensors.map(_buildSensorItem).toList(),\n      ),\n    );\n  }\n\n  Widget _buildSensorItem(SensorItem si) {\n    if (si.summary == null) {\n      return Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),\n        child: Text(si.device),\n      );\n    }\n\n    final itemW = Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Row(\n          children: [\n            Text(si.device, style: UIs.text15),\n            UIs.width7,\n            Text('(${si.adapter.raw})', style: UIs.text13Grey),\n          ],\n        ),\n        Text(si.summary ?? '', style: UIs.text13Grey),\n      ],\n    ).expanded();\n\n    return InkWell(\n      onTap: () => _onTapSensorItem(si),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),\n        child: Row(\n          crossAxisAlignment: CrossAxisAlignment.center,\n          children: [\n            itemW,\n            UIs.width7,\n            const Icon(Icons.keyboard_arrow_right, color: Colors.grey),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget? _buildPve(ServerState si) {\n    final addr = si.spi.custom?.pveAddr;\n    if (addr == null || addr.isEmpty) return null;\n    return CardX(\n      child: ListTile(\n        title: const Text('PVE'),\n        leading: const Icon(FontAwesome.server_solid, size: 17),\n        trailing: const Icon(Icons.chevron_right),\n        onTap: () => PvePage.route.go(context, PvePageArgs(spi: si.spi)),\n      ),\n    );\n  }\n\n  Widget? _buildCustomCmd(ServerState si) {\n    final ss = si.status;\n    if (ss.customCmds.isEmpty) return null;\n    return CardX(\n      child: ExpandTile(\n        leading: const Icon(MingCute.command_line, size: 17),\n        title: Text(l10n.customCmd),\n        initiallyExpanded: _getInitExpand(ss.customCmds.length),\n        children: ss.customCmds.entries.map(_buildCustomCmdItem).toList(),\n      ),\n    );\n  }\n\n  Widget _buildCustomCmdItem(MapEntry<String, String> cmd) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),\n      child: KvRow(\n        k: cmd.key,\n        v: cmd.value,\n        vBuilder: () {\n          if (!cmd.value.contains('\\n')) return null;\n          return GestureDetector(\n            onTap: () => _onTapCustomItem(cmd),\n            child: const Icon(Icons.info_outline, size: 17, color: Colors.grey),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildAnimatedText(Key key, String text, TextStyle style) {\n    return AnimatedSwitcher(\n      duration: const Duration(milliseconds: 277),\n      child: Text(key: key, text, style: style, textScaler: _textFactor),\n      transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/server/discovery/discovery.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/service/ssh_discovery.dart';\nimport 'package:server_box/data/model/server/discovery_result.dart';\n\npart 'widget.dart';\n\nclass SshDiscoveryPage extends ConsumerStatefulWidget {\n  const SshDiscoveryPage({super.key});\n\n  static const route = AppRouteNoArg<List<SshDiscoveryResult>>(\n    page: SshDiscoveryPage.new,\n    path: '/servers/discovery',\n  );\n\n  @override\n  ConsumerState<SshDiscoveryPage> createState() => _SshDiscoveryPageState();\n}\n\nclass _SshDiscoveryPageState extends ConsumerState<SshDiscoveryPage> {\n  final _config = ValueNotifier(const SshDiscoveryConfig());\n  final _discoveryResults = ValueNotifier<List<SshDiscoveryResult>>([]);\n  final _isDiscovering = ValueNotifier(false);\n  final _discoveryReport = ValueNotifier<SshDiscoveryReport?>(null);\n\n  @override\n  void dispose() {\n    _config.dispose();\n    _discoveryResults.dispose();\n    _isDiscovering.dispose();\n    _discoveryReport.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(\n        title: Text(l10n.discoverSshServers),\n        actions: [IconButton(icon: const Icon(Icons.settings), onPressed: _showSettings)],\n      ),\n      body: _buildBody(),\n      floatingActionButton: _isDiscovering.listenVal((discovering) {\n        if (discovering) return UIs.placeholder;\n        return _buildFAB();\n      }),\n    );\n  }\n\n  Widget _buildBody() {\n    return Column(\n      children: [\n        _buildSummary(),\n        Expanded(child: _buildResultsList()),\n      ],\n    );\n  }\n\n  Widget _buildSummary() {\n    return _discoveryReport.listenVal((report) {\n      if (report == null) {\n        return UIs.placeholder;\n      }\n\n      return Container(\n        padding: const EdgeInsets.all(16),\n        child: Card(\n          child: Padding(\n            padding: const EdgeInsets.all(16),\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(l10n.discoverySummary, style: const TextStyle(fontWeight: FontWeight.bold)),\n                UIs.height7,\n                Text('${libL10n.found}: ${report.count} ${libL10n.servers}'),\n                Text('${libL10n.duration}: ${report.durationMs}ms'),\n                Text(\n                  '${l10n.finishedAt}: ${DateTime.parse(report.generatedAt).toLocal().toString().substring(0, 16)}',\n                ),\n              ],\n            ),\n          ),\n        ),\n      );\n    });\n  }\n\n  Widget _buildResultsList() {\n    return _discoveryResults.listenVal((results) {\n      if (results.isEmpty) {\n        return _isDiscovering.listenVal((discovering) {\n          if (discovering) {\n            return const Center(\n              child: Column(\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [CircularProgressIndicator(), UIs.height13, Text('Discovering SSH servers...')],\n              ),\n            );\n          }\n          return Center(\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                Icon(BoxIcons.bx_search, size: 64, color: UIs.textGrey.color),\n                UIs.height13,\n                Text(l10n.tapToStartDiscovery, style: UIs.textGrey),\n              ],\n            ),\n          );\n        });\n      }\n\n      return ListView.builder(\n        itemCount: results.length,\n        itemBuilder: (context, index) {\n          final result = results[index];\n          return _buildResultTile(result, index);\n        },\n      );\n    });\n  }\n\n  Widget _buildResultTile(SshDiscoveryResult result, int index) {\n    return ListTile(\n      leading: Icon(\n        result.isSelected ? Icons.check_circle : Icons.circle_outlined,\n        color: result.isSelected ? Colors.green : null,\n      ),\n      title: Text(result.ip),\n      subtitle: result.banner != null\n          ? Text(result.banner!, style: const TextStyle(fontSize: 12))\n          : Text('Port ${result.port}', style: UIs.textGrey),\n      trailing: const Icon(BoxIcons.bx_server),\n      onTap: () {\n        final updated = result.copyWith(isSelected: !result.isSelected);\n        final newResults = List<SshDiscoveryResult>.from(_discoveryResults.value);\n        newResults[index] = updated;\n        _discoveryResults.value = newResults;\n      },\n    );\n  }\n\n  Widget _buildFAB() {\n    return _discoveryResults.listenVal((results) {\n      final selectedResults = results.where((r) => r.isSelected).toList();\n      return AnimatedSwitcher(\n        duration: const Duration(milliseconds: 400),\n        switchInCurve: Curves.easeInOut,\n        switchOutCurve: Curves.easeInOut,\n        transitionBuilder: (child, animation) {\n          return SlideTransition(\n            position: Tween<Offset>(\n              begin: const Offset(0, 0.3),\n              end: Offset.zero,\n            ).animate(CurvedAnimation(parent: animation, curve: Curves.easeInOutBack)),\n            child: FadeTransition(opacity: animation, child: child),\n          );\n        },\n        layoutBuilder: (currentChild, previousChildren) {\n          return Stack(\n            alignment: Alignment.centerRight,\n            children: <Widget>[...previousChildren, ?currentChild],\n          );\n        },\n        child: selectedResults.isNotEmpty\n            ? FloatingActionButton.extended(\n                key: const ValueKey('import'),\n                heroTag: 'import_fab',\n                onPressed: () => _importSelected(),\n                icon: const Icon(Icons.add),\n                label: Text('${libL10n.import} (${selectedResults.length})'),\n              )\n            : FloatingActionButton.extended(\n                key: const ValueKey('discovery'),\n                heroTag: 'discovery_fab',\n                onPressed: _startDiscovery,\n                icon: const Icon(BoxIcons.bx_search),\n                label: Text(libL10n.search),\n              ),\n      );\n    });\n  }\n\n  Future<void> _startDiscovery() async {\n    _isDiscovering.value = true;\n    _discoveryResults.value = [];\n    _discoveryReport.value = null;\n\n    try {\n      final report = await SshDiscoveryService.discover(_config.value);\n      _discoveryReport.value = report;\n      _discoveryResults.value = report.items;\n    } catch (e) {\n      if (mounted) {\n        context.showSnackBar('${l10n.discoveryFailed}: $e');\n      }\n    } finally {\n      _isDiscovering.value = false;\n    }\n  }\n\n  void _showSettings() {\n    context.showRoundDialog(\n      child: _DiscoverySettingsDialog(config: _config.value, onChanged: (config) => _config.value = config),\n      actions: Btnx.oks,\n    );\n  }\n\n  void _importSelected() {\n    final selected = _discoveryResults.value.where((r) => r.isSelected).toList();\n    if (selected.isEmpty) return;\n\n    context.pop(selected);\n  }\n}\n"
  },
  {
    "path": "lib/view/page/server/discovery/widget.dart",
    "content": "part of 'discovery.dart';\n\nclass _DiscoverySettingsDialog extends StatefulWidget {\n  final SshDiscoveryConfig config;\n  final ValueChanged<SshDiscoveryConfig> onChanged;\n\n  const _DiscoverySettingsDialog({\n    required this.config,\n    required this.onChanged,\n  });\n\n  @override\n  State<_DiscoverySettingsDialog> createState() => _DiscoverySettingsDialogState();\n}\n\nclass _DiscoverySettingsDialogState extends State<_DiscoverySettingsDialog> {\n  late final _timeoutController = TextEditingController(text: widget.config.timeoutMs.toString());\n  late final _concurrencyController = TextEditingController(text: widget.config.maxConcurrency.toString());\n  late bool _enableMdns = widget.config.enableMdns;\n\n  @override\n  void dispose() {\n    _timeoutController.dispose();\n    _concurrencyController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Text(\n          l10n.discoverySettings,\n          style: const TextStyle(fontSize: 18),\n        ),\n        UIs.height13,\n        Input(\n          controller: _timeoutController,\n          type: TextInputType.number,\n          label: '${libL10n.timeout} (ms)',\n          onChanged: (v) {\n            final t = int.tryParse(v) ?? 700;\n            if (t > 0) {\n              widget.onChanged(widget.config.copyWith(timeoutMs: t));\n            }\n          },\n          hint: '700',\n        ),\n        UIs.height7,\n        Input(\n          controller: _concurrencyController,\n          type: TextInputType.number,\n          label: l10n.maxConcurrency,\n          hint: '128',\n          onChanged: (v) {\n            final c = int.tryParse(v) ?? 128;\n            if (c > 0) {\n              widget.onChanged(widget.config.copyWith(maxConcurrency: c));\n            }\n          },\n        ),\n        UIs.height7,\n        SwitchListTile(\n          title: Text(l10n.enableMdns),\n          subtitle: Text(l10n.enableMdnsDesc),\n          value: _enableMdns,\n          onChanged: (value) {\n            setState(() {\n              _enableMdns = value;\n            });\n            widget.onChanged(widget.config.copyWith(enableMdns: value));\n          },\n        ),\n      ],\n    );\n  }\n}"
  },
  {
    "path": "lib/view/page/server/edit/actions.dart",
    "content": "part of 'edit.dart';\n\n/// Only permit ipv4 / ipv6 / domain chars (including IPv6 zone identifier like %en0)\nfinal _hostReg = RegExp(r'^[a-zA-Z0-9\\.\\-_:%;]+$');\n\nextension _Actions on _ServerEditPageState {\n  bool _isInvalidJumpSelection(String? candidateJumpId) {\n    final currentServer = spi;\n    return wouldCreateJumpCycle(\n      currentServerId: currentServer?.id,\n      candidateJumpId: candidateJumpId,\n      serversById: ref.read(serversProvider).servers,\n    );\n  }\n\n  void _onTapCustomItem() async {\n    final res = await KvEditor.route.go(\n      context,\n      KvEditorArgs(data: _customCmds.value),\n    );\n    if (res == null) return;\n    _customCmds.value = res;\n  }\n\n  void _onTapDisabledCmdTypes() async {\n    final allCmdTypes = ShellCmdType.all;\n\n    // [TimeSeq] depends on the `time` cmd type, so it should be removed from the list\n    allCmdTypes.remove(StatusCmdType.time);\n\n    await _showCmdTypesDialog(allCmdTypes);\n  }\n\n  void _onSave() async {\n    if (_ipController.text.isEmpty) {\n      context.showSnackBar('${libL10n.empty} ${libL10n.host}');\n      return;\n    }\n\n    if (!_hostReg.hasMatch(_ipController.text)) {\n      context.showSnackBar(l10n.invalidHostFormat);\n      return;\n    }\n\n    if (_keyIdx.value == null && _passwordController.text.isEmpty) {\n      final ok = await context.showRoundDialog<bool>(\n        title: libL10n.attention,\n        child: Text(libL10n.askContinue(l10n.useNoPwd)),\n        actions: Btnx.cancelRedOk,\n      );\n      if (ok != true) return;\n    }\n\n    // If [_pubKeyIndex] is -1, it means that the user has not selected\n    if (_keyIdx.value == -1) {\n      context.showSnackBar(libL10n.empty);\n      return;\n    }\n    if (_usernameController.text.isEmpty) {\n      _usernameController.text = 'root';\n    }\n    if (_portController.text.isEmpty) {\n      _portController.text = '22';\n    }\n    if (_isInvalidJumpSelection(_jumpServer.value)) {\n      context.showSnackBar('${l10n.invalid}: ${l10n.jumpServer}');\n      return;\n    }\n\n    final customCmds = _customCmds.value;\n    final custom = ServerCustom(\n      pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,\n      pveIgnoreCert: _pveIgnoreCert.value,\n      cmds: customCmds.isEmpty ? null : customCmds,\n      preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,\n      logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull,\n      netDev: _netDevCtrl.text.selfNotEmptyOrNull,\n      scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull,\n    );\n\n    final wolEmpty =\n        _wolMacCtrl.text.isEmpty &&\n        _wolIpCtrl.text.isEmpty &&\n        _wolPwdCtrl.text.isEmpty;\n    final wol = wolEmpty\n        ? null\n        : WakeOnLanCfg(\n            mac: _wolMacCtrl.text,\n            ip: _wolIpCtrl.text,\n            pwd: _wolPwdCtrl.text.selfNotEmptyOrNull,\n          );\n    if (wol != null) {\n      final wolValidation = wol.validate();\n      if (!wolValidation.$2) {\n        context.showSnackBar('${libL10n.fail}: ${wolValidation.$1}');\n        return;\n      }\n    }\n\n    final spi = Spi(\n      name: _nameController.text.isEmpty\n          ? _ipController.text\n          : _nameController.text,\n      ip: _ipController.text,\n      port: int.parse(_portController.text),\n      user: _usernameController.text,\n      pwd: _passwordController.text.selfNotEmptyOrNull,\n      keyId: _keyIdx.value != null\n          ? ref.read(privateKeyProvider).keys.elementAt(_keyIdx.value!).id\n          : null,\n      tags: _tags.value.isEmpty ? null : _tags.value.toList(),\n      alterUrl: _altUrlController.text.selfNotEmptyOrNull,\n      autoConnect: _autoConnect.value,\n      jumpId: _jumpServer.value,\n      custom: custom,\n      wolCfg: wol,\n      envs: _env.value.isEmpty ? null : _env.value,\n      id: widget.args?.spi.id ?? ShortId.generate(),\n      customSystemType: _systemType.value,\n      disabledCmdTypes: _disabledCmdTypes.value.isEmpty\n          ? null\n          : _disabledCmdTypes.value.toList(),\n    );\n\n    if (this.spi == null) {\n      final existsIds = ServerStore.instance.box.keys;\n      if (existsIds.contains(spi.id)) {\n        context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');\n        return;\n      }\n      ref.read(serversProvider.notifier).addServer(spi);\n    } else {\n      ref.read(serversProvider.notifier).updateServer(this.spi!, spi);\n    }\n\n    context.pop();\n  }\n}\n\nextension _Utils on _ServerEditPageState {\n  Future<void> _checkSSHConfigImport() async {\n    final hasExistingServers = ref.read(serversProvider).servers.isNotEmpty;\n    if (hasExistingServers) {\n      Stores.setting.firstTimeReadSSHCfg.put(false);\n      return;\n    }\n\n    try {\n      final servers = await SSHConfig.parseConfig();\n      if (!mounted) return;\n      if (servers.isEmpty) {\n        Stores.setting.firstTimeReadSSHCfg.put(false);\n        return;\n      }\n\n      final shouldImport = await context.showRoundDialog<bool>(\n        title: l10n.sshConfigImport,\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(l10n.sshConfigFound),\n            const SizedBox(height: 8),\n            Text(l10n.sshConfigImportPermission),\n          ],\n        ),\n        actions: Btnx.cancelOk,\n      );\n\n      if (!mounted) return;\n\n      Stores.setting.firstTimeReadSSHCfg.put(false);\n\n      if (shouldImport == true) {\n        await ServerDeduplication.importServersWithNotification(\n          servers: servers,\n          ref: ref,\n          context: context,\n          allExistMessage: l10n.sshConfigAllExist,\n          importedMessage: l10n.sshConfigImported,\n        );\n      }\n    } catch (e) {\n      if (!mounted) return;\n      if (e is PathAccessException ||\n          e.toString().contains('Operation not permitted')) {\n        Stores.setting.firstTimeReadSSHCfg.put(false);\n        context.showSnackBar(\n          '${l10n.sshConfigPermissionDenied} ${l10n.sshConfigManualSelect}',\n        );\n      } else {\n        dprint('Error checking SSH config: $e');\n        Stores.setting.firstTimeReadSSHCfg.put(false);\n      }\n    }\n  }\n\n  Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {\n    return context.showRoundDialog(\n      title: '${libL10n.disabled} ${libL10n.cmd}',\n      child: SizedBox(\n        width: 270,\n        child: _disabledCmdTypes.listenVal((disabled) {\n          return ListView.builder(\n            itemCount: allCmdTypes.length,\n            itemExtent: 50,\n            itemBuilder: (context, index) {\n              final cmdType = allCmdTypes.elementAtOrNull(index);\n              if (cmdType == null) return UIs.placeholder;\n              final display = cmdType.displayName;\n              return ListTile(\n                leading: Icon(cmdType.sysType.icon, size: 20),\n                title: Text(cmdType.name, style: const TextStyle(fontSize: 16)),\n                trailing: Checkbox(\n                  value: disabled.contains(display),\n                  onChanged: (value) {\n                    if (value == null) return;\n                    if (value) {\n                      _disabledCmdTypes.value.add(display);\n                    } else {\n                      _disabledCmdTypes.value.remove(display);\n                    }\n                    _disabledCmdTypes.notify();\n                  },\n                ),\n                onTap: () {\n                  final isDisabled = disabled.contains(display);\n                  if (isDisabled) {\n                    _disabledCmdTypes.value.remove(display);\n                  } else {\n                    _disabledCmdTypes.value.add(display);\n                  }\n                  _disabledCmdTypes.notify();\n                },\n              );\n            },\n          );\n        }),\n      ),\n      actions: Btnx.oks,\n    );\n  }\n\n  void _initWithSpi(Spi spi) {\n    _nameController.text = spi.name;\n    _ipController.text = spi.ip;\n    _portController.text = spi.port.toString();\n    _usernameController.text = spi.user;\n    if (spi.keyId == null) {\n      _passwordController.text = spi.pwd ?? '';\n    } else {\n      _keyIdx.value = ref\n          .read(privateKeyProvider)\n          .keys\n          .indexWhere((e) => e.id == spi.keyId);\n    }\n\n    /// List in dart is passed by pointer, so you need to copy it here\n    _tags.value = spi.tags?.toSet() ?? {};\n\n    _altUrlController.text = spi.alterUrl ?? '';\n    _autoConnect.value = spi.autoConnect;\n    _jumpServer.value = spi.jumpId;\n\n    final custom = spi.custom;\n    if (custom != null) {\n      _pveAddrCtrl.text = custom.pveAddr ?? '';\n      _pveIgnoreCert.value = custom.pveIgnoreCert;\n      _customCmds.value = custom.cmds ?? {};\n      _preferTempDevCtrl.text = custom.preferTempDev ?? '';\n      _logoUrlCtrl.text = custom.logoUrl ?? '';\n    }\n\n    final wol = spi.wolCfg;\n    if (wol != null) {\n      _wolMacCtrl.text = wol.mac;\n      _wolIpCtrl.text = wol.ip;\n      _wolPwdCtrl.text = wol.pwd ?? '';\n    }\n\n    _env.value = spi.envs ?? {};\n\n    _netDevCtrl.text = spi.custom?.netDev ?? '';\n    _scriptDirCtrl.text = spi.custom?.scriptDir ?? '';\n\n    _systemType.value = spi.customSystemType;\n\n    final disabledCmdTypes = spi.disabledCmdTypes?.toSet() ?? {};\n    final allAvailableCmdTypes = ShellCmdType.all.map((e) => e.displayName);\n    disabledCmdTypes.removeWhere((e) => !allAvailableCmdTypes.contains(e));\n    _disabledCmdTypes.value = disabledCmdTypes;\n  }\n}\n"
  },
  {
    "path": "lib/view/page/server/edit/edit.dart",
    "content": "import 'dart:io';\n\nimport 'package:choice/choice.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/route.dart';\nimport 'package:server_box/core/utils/jump_chain.dart';\nimport 'package:server_box/core/utils/server_dedup.dart';\nimport 'package:server_box/core/utils/ssh_config.dart';\nimport 'package:server_box/data/model/app/scripts/cmd_types.dart';\nimport 'package:server_box/data/model/server/custom.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/system.dart';\nimport 'package:server_box/data/model/server/wol_cfg.dart';\nimport 'package:server_box/data/provider/private_key.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/data/store/server.dart';\nimport 'package:server_box/view/page/private_key/edit.dart';\n\npart 'actions.dart';\npart 'widget.dart';\n\nclass ServerEditPage extends ConsumerStatefulWidget {\n  final SpiRequiredArgs? args;\n\n  const ServerEditPage({super.key, this.args});\n\n  static const route = AppRoute<bool, SpiRequiredArgs>(\n    page: ServerEditPage.new,\n    path: '/servers/edit',\n  );\n\n  @override\n  ConsumerState<ServerEditPage> createState() => _ServerEditPageState();\n}\n\nclass _ServerEditPageState extends ConsumerState<ServerEditPage>\n    with AfterLayoutMixin {\n  late final spi = widget.args?.spi;\n  final _nameController = TextEditingController();\n  final _ipController = TextEditingController();\n  final _altUrlController = TextEditingController();\n  final _portController = TextEditingController();\n  final _usernameController = TextEditingController();\n  final _passwordController = TextEditingController();\n  final _pveAddrCtrl = TextEditingController();\n  final _preferTempDevCtrl = TextEditingController();\n  final _logoUrlCtrl = TextEditingController();\n  final _wolMacCtrl = TextEditingController();\n  final _wolIpCtrl = TextEditingController();\n  final _wolPwdCtrl = TextEditingController();\n  final _netDevCtrl = TextEditingController();\n  final _scriptDirCtrl = TextEditingController();\n\n  final _nameFocus = FocusNode();\n  final _ipFocus = FocusNode();\n  final _alterUrlFocus = FocusNode();\n  final _portFocus = FocusNode();\n  final _usernameFocus = FocusNode();\n\n  late FocusScopeNode _focusScope;\n\n  /// -1: non selected, null: password, others: index of private key\n  final _keyIdx = ValueNotifier<int?>(null);\n  final _autoConnect = ValueNotifier(true);\n  final _jumpServer = nvn<String?>();\n  final _pveIgnoreCert = ValueNotifier(false);\n  final _env = <String, String>{}.vn;\n  final _customCmds = <String, String>{}.vn;\n  final _tags = <String>{}.vn;\n  final _systemType = ValueNotifier<SystemType?>(null);\n  final _disabledCmdTypes = <String>{}.vn;\n\n  @override\n  void dispose() {\n    super.dispose();\n    _nameController.dispose();\n    _ipController.dispose();\n    _altUrlController.dispose();\n    _portController.dispose();\n    _usernameController.dispose();\n    _passwordController.dispose();\n    _preferTempDevCtrl.dispose();\n    _logoUrlCtrl.dispose();\n    _wolMacCtrl.dispose();\n    _wolIpCtrl.dispose();\n    _wolPwdCtrl.dispose();\n    _netDevCtrl.dispose();\n    _scriptDirCtrl.dispose();\n\n    _nameFocus.dispose();\n    _ipFocus.dispose();\n    _alterUrlFocus.dispose();\n    _portFocus.dispose();\n    _usernameFocus.dispose();\n    _pveAddrCtrl.dispose();\n\n    _keyIdx.dispose();\n    _autoConnect.dispose();\n    _jumpServer.dispose();\n    _pveIgnoreCert.dispose();\n    _env.dispose();\n    _customCmds.dispose();\n    _tags.dispose();\n    _systemType.dispose();\n    _disabledCmdTypes.dispose();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    _focusScope = FocusScope.of(context);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final actions = <Widget>[];\n    if (spi != null) actions.add(_buildDelBtn());\n\n    return GestureDetector(\n      onTap: () => _focusScope.unfocus(),\n      child: Scaffold(\n        appBar: CustomAppBar(title: Text(libL10n.edit), actions: actions),\n        body: _buildForm(),\n        floatingActionButton: _buildFAB(),\n      ),\n    );\n  }\n\n  Widget _buildForm() {\n    final topItems = [\n      _buildWriteScriptTip(),\n    ];\n    final children = [\n      SizedBox(\n        height: 50,\n        child: ListView(\n          scrollDirection: Axis.horizontal,\n          children: topItems.joinWith(UIs.width13).toList(),\n        ),\n      ),\n      Input(\n        autoFocus: true,\n        controller: _nameController,\n        type: TextInputType.text,\n        node: _nameFocus,\n        onSubmitted: (_) => _focusScope.requestFocus(_ipFocus),\n        hint: libL10n.example,\n        label: libL10n.name,\n        icon: BoxIcons.bx_rename,\n        obscureText: false,\n        autoCorrect: true,\n        suggestion: true,\n      ),\n      Input(\n        controller: _ipController,\n        type: TextInputType.url,\n        onSubmitted: (_) => _focusScope.requestFocus(_portFocus),\n        node: _ipFocus,\n        label: libL10n.host,\n        icon: BoxIcons.bx_server,\n        hint: 'example.com',\n        suggestion: false,\n      ),\n      Input(\n        controller: _portController,\n        type: TextInputType.number,\n        node: _portFocus,\n        onSubmitted: (_) => _focusScope.requestFocus(_usernameFocus),\n        label: libL10n.port,\n        icon: Bootstrap.number_123,\n        hint: '22',\n        suggestion: false,\n      ),\n      Input(\n        controller: _usernameController,\n        type: TextInputType.text,\n        node: _usernameFocus,\n        onSubmitted: (_) => _focusScope.requestFocus(_alterUrlFocus),\n        label: libL10n.user,\n        icon: Icons.account_box,\n        hint: 'root',\n        suggestion: false,\n      ),\n      TagTile(tags: _tags, allTags: ref.watch(serversProvider).tags).cardx,\n      ListTile(\n        title: Text(l10n.autoConnect),\n        trailing: _autoConnect.listenVal(\n          (val) => Switch(\n            value: val,\n            onChanged: (val) {\n              _autoConnect.value = val;\n            },\n          ),\n        ),\n      ),\n      _buildAuth(),\n      _buildSystemType(),\n      _buildJumpServer(),\n      _buildMore(),\n    ];\n    return AutoMultiList(children: children);\n  }\n\n  @override\n  void afterFirstLayout(BuildContext context) {\n    if (spi != null) {\n      _initWithSpi(spi!);\n    } else if (isDesktop && Stores.setting.firstTimeReadSSHCfg.fetch()) {\n      _checkSSHConfigImport();\n    }\n  }\n}\n"
  },
  {
    "path": "lib/view/page/server/edit/widget.dart",
    "content": "part of 'edit.dart';\n\nextension _Widgets on _ServerEditPageState {\n  Widget _buildAuth() {\n    final switch_ = ListTile(\n      title: Text(l10n.keyAuth),\n      trailing: _keyIdx.listenVal(\n        (v) => Switch(\n          value: v != null,\n          onChanged: (val) {\n            if (val) {\n              _keyIdx.value = -1;\n            } else {\n              _keyIdx.value = null;\n            }\n          },\n        ),\n      ),\n    );\n\n    /// Put [switch_] out of [ValueBuilder] to avoid rebuild\n    return _keyIdx.listenVal((v) {\n      final children = <Widget>[switch_];\n      if (v != null) {\n        children.add(_buildKeyAuth());\n      } else {\n        children.add(\n          Input(\n            controller: _passwordController,\n            obscureText: true,\n            type: TextInputType.text,\n            label: libL10n.pwd,\n            icon: Icons.password,\n            suggestion: false,\n            onSubmitted: (_) => _onSave(),\n          ),\n        );\n      }\n      return Column(children: children);\n    });\n  }\n\n  Widget _buildKeyAuth() {\n    const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);\n    final privateKeyState = ref.watch(privateKeyProvider);\n    final pkis = privateKeyState.keys;\n\n    final choice = _keyIdx.listenVal((val) {\n      final selectedPki = val != null && val >= 0 && val < pkis.length\n          ? pkis[val]\n          : null;\n      return Choice<int>(\n        multiple: false,\n        clearable: true,\n        value: selectedPki != null ? [val!] : [],\n        builder: (state, _) => Column(\n          children: [\n            Wrap(\n              children: List<Widget>.generate(pkis.length, (index) {\n                final item = pkis[index];\n                return ChoiceChipX<int>(\n                  key: ValueKey(index),\n                  label: item.id,\n                  state: state,\n                  value: index,\n                  onSelected: (idx, on) {\n                    if (on) {\n                      _keyIdx.value = idx;\n                    } else {\n                      _keyIdx.value = -1;\n                    }\n                  },\n                );\n              }),\n            ),\n            UIs.height7,\n            Row(\n              mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n              children: [\n                if (selectedPki != null)\n                  Btn.icon(\n                    icon: const Icon(Icons.edit, size: 20),\n                    text: libL10n.edit,\n                    onTap: () => PrivateKeyEditPage.route.go(\n                      context,\n                      args: PrivateKeyEditPageArgs(pki: selectedPki),\n                    ),\n                  ),\n                Btn.icon(\n                  icon: const Icon(Icons.add, size: 20),\n                  text: libL10n.add,\n                  onTap: () => PrivateKeyEditPage.route.go(context),\n                ),\n              ],\n            ),\n          ],\n        ),\n      );\n    });\n\n    return ExpandTile(\n      leading: const Icon(Icons.key),\n      initiallyExpanded: _keyIdx.value != null && _keyIdx.value! >= 0,\n      childrenPadding: padding,\n      title: Text(l10n.privateKey),\n      children: [choice],\n    ).cardx;\n  }\n\n  Widget _buildEnvs() {\n    return _env.listenVal((val) {\n      final subtitle = val.isEmpty\n          ? null\n          : Text(val.keys.join(','), style: UIs.textGrey);\n      return ListTile(\n        leading: const Icon(HeroIcons.variable),\n        subtitle: subtitle,\n        title: Text(l10n.envVars),\n        trailing: const Icon(Icons.keyboard_arrow_right),\n        onTap: () async {\n          final res = await KvEditor.route.go(\n            context,\n            KvEditorArgs(data: spi?.envs ?? {}),\n          );\n          if (res == null) return;\n          _env.value = res;\n        },\n      ).cardx;\n    });\n  }\n\n  Widget _buildMore() {\n    return ExpandTile(\n      title: Text(l10n.more),\n      children: [\n        Input(\n          controller: _logoUrlCtrl,\n          type: TextInputType.url,\n          icon: Icons.image,\n          label: 'Logo URL',\n          hint: 'https://example.com/logo.png',\n          suggestion: false,\n        ),\n        _buildAltUrl(),\n        _buildScriptDir(),\n        _buildEnvs(),\n        _buildPVEs(),\n        _buildCustomCmds(),\n        _buildDisabledCmdTypes(),\n        _buildCustomDev(),\n        _buildWOLs(),\n      ],\n    );\n  }\n\n  Widget _buildScriptDir() {\n    return Input(\n      controller: _scriptDirCtrl,\n      type: TextInputType.text,\n      label: '${l10n.remotePath} (Shell ${libL10n.install})',\n      icon: Icons.folder,\n      hint: '~/.config/server_box',\n      suggestion: false,\n    );\n  }\n\n  Widget _buildCustomDev() {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        CenterGreyTitle(l10n.specifyDev),\n        ListTile(\n          leading: const Icon(MingCute.question_line),\n          title: TipText(libL10n.note, l10n.specifyDevTip),\n        ).cardx,\n        Input(\n          controller: _preferTempDevCtrl,\n          type: TextInputType.text,\n          label: libL10n.temperature,\n          icon: MingCute.low_temperature_line,\n          hint: 'nvme-pci-0400',\n          suggestion: false,\n        ),\n        Input(\n          controller: _netDevCtrl,\n          type: TextInputType.text,\n          label: libL10n.net,\n          icon: ZondIcons.network,\n          hint: 'eth0',\n          suggestion: false,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildSystemType() {\n    return _systemType.listenVal((val) {\n      return ListTile(\n        leading: Icon(MingCute.laptop_2_line),\n        title: Text(l10n.system),\n        trailing: PopupMenu<SystemType?>(\n          initialValue: val,\n          items: [\n            PopupMenuItem(value: null, child: Text(libL10n.auto)),\n            PopupMenuItem(value: SystemType.linux, child: Text('Linux')),\n            PopupMenuItem(value: SystemType.bsd, child: Text('BSD')),\n            PopupMenuItem(value: SystemType.windows, child: Text('Windows')),\n          ],\n          onSelected: (value) => _systemType.value = value,\n          child: Text(\n            val?.name ?? libL10n.auto,\n            style: TextStyle(color: val == null ? Colors.grey : null),\n          ),\n        ),\n      ).cardx;\n    });\n  }\n\n  Widget _buildAltUrl() {\n    return Input(\n      controller: _altUrlController,\n      type: TextInputType.url,\n      node: _alterUrlFocus,\n      label: l10n.fallbackSshDest,\n      icon: MingCute.link_line,\n      hint: 'user@ip:port',\n      suggestion: false,\n    );\n  }\n\n  Widget _buildPVEs() {\n    const addr = 'https://127.0.0.1:8006';\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        const CenterGreyTitle('PVE'),\n        Input(\n          controller: _pveAddrCtrl,\n          type: TextInputType.url,\n          icon: MingCute.web_line,\n          label: 'URL',\n          hint: addr,\n          suggestion: false,\n        ),\n        ListTile(\n          leading: const Icon(MingCute.certificate_line),\n          title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip),\n          trailing: _pveIgnoreCert.listenVal(\n            (v) => Switch(\n              value: v,\n              onChanged: (val) {\n                _pveIgnoreCert.value = val;\n              },\n            ),\n          ),\n        ).cardx,\n      ],\n    );\n  }\n\n  Widget _buildCustomCmds() {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        CenterGreyTitle(l10n.customCmd),\n        _customCmds.listenVal((vals) {\n          return ListTile(\n            leading: const Icon(BoxIcons.bxs_file_json),\n            title: const Text('JSON'),\n            subtitle: vals.isEmpty\n                ? null\n                : Text(vals.keys.join(','), style: UIs.textGrey),\n            trailing: const Icon(Icons.keyboard_arrow_right),\n            onTap: _onTapCustomItem,\n          );\n        }).cardx,\n        ListTile(\n          leading: const Icon(MingCute.doc_line),\n          title: Text(libL10n.doc),\n          trailing: const Icon(Icons.open_in_new, size: 17),\n          onTap: libL10n.customCmdDocUrl.launchUrl,\n        ).cardx,\n      ],\n    );\n  }\n\n  Widget _buildDisabledCmdTypes() {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        CenterGreyTitle('${libL10n.disabled} ${libL10n.cmd}'),\n        _disabledCmdTypes.listenVal((disabled) {\n          return ListTile(\n            leading: const Icon(Icons.disabled_by_default),\n            title: Text('${libL10n.disabled} ${libL10n.cmd}'),\n            subtitle: disabled.isEmpty\n                ? null\n                : Text(disabled.join(', '), style: UIs.textGrey),\n            trailing: const Icon(Icons.keyboard_arrow_right),\n            onTap: _onTapDisabledCmdTypes,\n          );\n        }).cardx,\n      ],\n    );\n  }\n\n  Widget _buildWOLs() {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        const CenterGreyTitle('Wake On LAN (beta)'),\n        ListTile(\n          leading: const Icon(BoxIcons.bxs_help_circle),\n          title: TipText(libL10n.about, l10n.wolTip),\n        ).cardx,\n        Input(\n          controller: _wolMacCtrl,\n          type: TextInputType.text,\n          label: 'MAC ${l10n.addr}',\n          icon: Icons.computer,\n          hint: '00:11:22:33:44:55',\n          suggestion: false,\n        ),\n        Input(\n          controller: _wolIpCtrl,\n          type: TextInputType.text,\n          label: 'IP ${l10n.addr}',\n          icon: ZondIcons.network,\n          hint: '192.168.1.x',\n          suggestion: false,\n        ),\n        Input(\n          controller: _wolPwdCtrl,\n          type: TextInputType.text,\n          obscureText: true,\n          label: libL10n.pwd,\n          icon: Icons.password,\n          suggestion: false,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildFAB() {\n    return FloatingActionButton(\n      onPressed: _onSave,\n      child: const Icon(Icons.save),\n    );\n  }\n\n  Widget _buildJumpServer() {\n    const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);\n    final srvs = ref\n        .watch(serversProvider)\n        .servers\n        .values\n        .where((e) => e.id != spi?.id)\n        .where((e) => !_isInvalidJumpSelection(e.id))\n        .toList();\n    final choice = _jumpServer.listenVal((val) {\n      final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);\n      return Choice<Spi>(\n        multiple: false,\n        clearable: true,\n        value: srv != null ? [srv] : [],\n        builder: (state, _) => Wrap(\n          children: List<Widget>.generate(srvs.length, (index) {\n            final item = srvs[index];\n            return ChoiceChipX<Spi>(\n              key: ValueKey(item),\n              label: item.name,\n              state: state,\n              value: item,\n              onSelected: (srv, on) {\n                if (on) {\n                  _jumpServer.value = srv.id;\n                } else {\n                  _jumpServer.value = null;\n                }\n              },\n            );\n          }),\n        ),\n      );\n    });\n    return ExpandTile(\n      leading: const Icon(Icons.map),\n      initiallyExpanded: _jumpServer.value != null,\n      childrenPadding: padding,\n      title: Text(l10n.jumpServer),\n      children: [choice],\n    ).cardx;\n  }\n\n  Widget _buildWriteScriptTip() {\n    return Btn.tile(\n      text: libL10n.attention,\n      icon: const Icon(Icons.tips_and_updates, color: Colors.grey),\n      onTap: () {\n        context.showRoundDialog(\n          title: libL10n.attention,\n          child: SimpleMarkdown(data: l10n.writeScriptTip),\n          actions: Btnx.oks,\n        );\n      },\n      textStyle: UIs.textGrey,\n      mainAxisSize: MainAxisSize.min,\n    );\n  }\n\n  Widget _buildDelBtn() {\n    return IconButton(\n      onPressed: () {\n        context.showRoundDialog(\n          title: libL10n.attention,\n          child: Text(\n            libL10n.askContinue(\n              '${libL10n.delete} ${libL10n.server}(${spi!.name})',\n            ),\n          ),\n          actions: Btn.ok(\n            onTap: () async {\n              context.pop();\n              ref.read(serversProvider.notifier).delServer(spi!.id);\n              context.pop(true);\n            },\n            red: true,\n          ).toList,\n        );\n      },\n      icon: const Icon(Icons.delete),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/server/tab/card_stat.dart",
    "content": "part of 'tab.dart';\n\ntypedef _CardNotifier = ValueNotifier<_CardStatus>;\n\nclass _CardStatus {\n  final bool flip;\n  final bool? diskIO;\n  final NetViewType? net;\n\n  const _CardStatus({this.flip = false, this.diskIO, this.net});\n\n  _CardStatus copyWith({bool? flip, bool? diskIO, NetViewType? net}) {\n    return _CardStatus(flip: flip ?? this.flip, diskIO: diskIO ?? this.diskIO, net: net ?? this.net);\n  }\n}\n"
  },
  {
    "path": "lib/view/page/server/tab/content.dart",
    "content": "part of 'tab.dart';\n\nextension on _ServerPageState {\n  Widget _buildServerCardTitle(ServerState s) {\n    return Padding(\n      padding: const EdgeInsets.only(left: 7, right: 13),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          Text(s.spi.name, style: UIs.text13Bold, maxLines: 1, overflow: TextOverflow.ellipsis),\n          const Icon(Icons.keyboard_arrow_right, size: 17, color: Colors.grey),\n          const Spacer(),\n          _buildTopRightText(s),\n          _buildTopRightWidget(s),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildTopRightWidget(ServerState s) {\n    final (child, onTap) = switch (s.conn) {\n      ServerConn.connecting || ServerConn.loading || ServerConn.connected => (\n        SizedBox.square(\n          dimension: _ServerPageState._kCardHeightMin,\n          child: SizedLoading(_ServerPageState._kCardHeightMin, padding: 3),\n        ),\n        null,\n      ),\n      ServerConn.failed => (\n        const Icon(Icons.refresh, size: 21, color: Colors.grey),\n        () {\n          TryLimiter.reset(s.spi.id);\n          ref.read(serversProvider.notifier).refresh(spi: s.spi);\n        },\n      ),\n      ServerConn.disconnected => (\n        const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),\n        () => ref.read(serversProvider.notifier).refresh(spi: s.spi),\n      ),\n      ServerConn.finished => (\n        const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),\n        () => ref.read(serversProvider.notifier).closeServer(id: s.spi.id),\n      ),\n    };\n\n    // Or the loading icon will be rescaled.\n    final wrapped = child is SizedBox\n        ? child\n        : SizedBox(height: _ServerPageState._kCardHeightMin, width: 27, child: child);\n    if (onTap == null) return wrapped.paddingOnly(left: 10);\n    return InkWell(borderRadius: BorderRadius.circular(7), onTap: onTap, child: wrapped).paddingOnly(left: 5);\n  }\n\n  Widget _buildTopRightText(ServerState s) {\n    final hasErr = s.status.err != null;\n    final str = s._getTopRightStr(s.spi);\n    if (str == null) return UIs.placeholder;\n    return GestureDetector(\n      onTap: () {\n        if (!hasErr) return;\n        _showFailReason(s.status);\n      },\n      child: Text(str, style: UIs.text13Grey),\n    );\n  }\n\n  void _showFailReason(ServerStatus ss) {\n    final md =\n        '''\n${ss.err?.solution ?? l10n.unknown}\n\n```sh\n${ss.err?.message ?? 'null'}\n''';\n    context.showRoundDialog(\n      title: libL10n.error,\n      child: SingleChildScrollView(child: SimpleMarkdown(data: md)),\n      actions: [TextButton(onPressed: () => Pfs.copy(md), child: Text(libL10n.copy))],\n    );\n  }\n\n  Widget _buildDisk(ServerStatus ss, String id) {\n    final cardNoti = _getCardNoti(id);\n    return cardNoti.listenVal((v) {\n      final isSpeed = v.diskIO ?? !Stores.setting.serverTabPreferDiskAmount.fetch();\n\n      final (r, w) = ss.diskIO.cachedAllSpeed;\n\n      return AnimatedSwitcher(\n        duration: const Duration(milliseconds: 377),\n        transitionBuilder: (child, animation) {\n          return FadeTransition(opacity: animation, child: child);\n        },\n        child: _buildIOData(\n          isSpeed ? '${l10n.read}:\\n$r' : 'Total:\\n${ss.diskUsage?.size.kb2Str}',\n          isSpeed ? '${l10n.write}:\\n$w' : 'Used:\\n${ss.diskUsage?.used.kb2Str}',\n          onTap: () {\n            cardNoti.value = v.copyWith(diskIO: !isSpeed);\n          },\n          key: ValueKey(isSpeed),\n        ),\n      );\n    });\n  }\n\n  Widget _buildNet(ServerStatus ss, String id) {\n    final cardNoti = _getCardNoti(id);\n    final type = cardNoti.value.net ?? Stores.setting.netViewType.fetch();\n    final device = ref.watch(serversProvider).servers[id]?.custom?.netDev;\n    final (a, b) = type.build(ss, dev: device);\n    return AnimatedSwitcher(\n      duration: const Duration(milliseconds: 377),\n      transitionBuilder: (c, anim) => FadeTransition(opacity: anim, child: c),\n      child: _buildIOData(\n        a,\n        b,\n        onTap: () => cardNoti.value = cardNoti.value.copyWith(net: type.next),\n        key: ValueKey(type),\n      ),\n    );\n  }\n\n  Widget _buildIOData(String up, String down, {void Function()? onTap, Key? key, int maxLines = 2}) {\n    final child = Column(\n      mainAxisSize: MainAxisSize.min,\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        Text(\n          up,\n          style: const TextStyle(fontSize: 10, color: Colors.grey),\n          textAlign: TextAlign.center,\n          textScaler: _textFactor,\n          maxLines: maxLines,\n        ),\n        const SizedBox(height: 3),\n        Text(\n          down,\n          style: const TextStyle(fontSize: 10, color: Colors.grey),\n          textAlign: TextAlign.center,\n          textScaler: _textFactor,\n          maxLines: maxLines,\n        ),\n      ],\n    );\n    if (onTap == null) return child;\n    return IconButton(\n      key: key,\n      padding: const EdgeInsets.symmetric(horizontal: 3),\n      onPressed: onTap,\n      icon: child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/server/tab/landscape.dart",
    "content": "part of 'tab.dart';\n\nextension on _ServerPageState {\n  Widget _buildLandscape() {\n    final offset = Offset(_offset, _offset);\n    return Padding(\n      // Avoid display cutout\n      padding: EdgeInsets.all(_offset.abs()),\n      child: Transform.translate(\n        offset: offset,\n        child: Stack(\n          children: [\n            _buildLandscapeBody(),\n            Positioned(\n              top: 0,\n              left: 0,\n              child: IconButton(\n                onPressed: () => SettingsPage.route.go(context),\n                icon: const Icon(Icons.settings, color: Colors.grey),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildLandscapeBody() {\n    final serverState = ref.watch(serversProvider);\n    final order = serverState.serverOrder;\n    \n    if (order.isEmpty) {\n      return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));\n    }\n\n    return PageView.builder(\n      itemCount: order.length,\n      itemBuilder: (_, idx) {\n        final id = order[idx];\n        final srv = ref.watch(serverProvider(id));\n\n        final title = _buildServerCardTitle(srv);\n        final List<Widget> children = [title, _buildNormalCard(srv.status, srv.spi)];\n\n        return _getCardNoti(id).listenVal((_) {\n          return Column(\n            mainAxisSize: MainAxisSize.min,\n            mainAxisAlignment: MainAxisAlignment.center,\n            crossAxisAlignment: CrossAxisAlignment.center,\n            children: children,\n          );\n        });\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/server/tab/tab.dart",
    "content": "// ignore_for_file: invalid_use_of_protected_member\n\nimport 'dart:async';\nimport 'dart:math' as math;\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:responsive_framework/responsive_framework.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/extension/ssh_client.dart';\nimport 'package:server_box/core/route.dart';\nimport 'package:server_box/data/model/app/net_view.dart';\nimport 'package:server_box/data/model/app/scripts/cmd_types.dart';\nimport 'package:server_box/data/model/app/scripts/shell_func.dart';\nimport 'package:server_box/data/model/server/server.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/try_limiter.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/provider/server/single.dart';\nimport 'package:server_box/data/res/build_data.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/view/page/server/detail/view.dart';\nimport 'package:server_box/view/page/server/edit/edit.dart';\nimport 'package:server_box/view/page/setting/entry.dart';\nimport 'package:server_box/view/widget/percent_circle.dart';\nimport 'package:server_box/view/widget/server_func_btns.dart';\n\npart 'card_stat.dart';\npart 'content.dart';\npart 'landscape.dart';\npart 'top_bar.dart';\npart 'utils.dart';\n\nclass ServerPage extends ConsumerStatefulWidget {\n  const ServerPage({super.key});\n\n  @override\n  ConsumerState<ServerPage> createState() => _ServerPageState();\n\n  static const route = AppRouteNoArg(page: ServerPage.new, path: '/servers');\n}\n\nconst _cardPad = 74.0;\nconst _cardPadSingle = 13.0;\n\nclass _ServerPageState extends ConsumerState<ServerPage>\n    with AutomaticKeepAliveClientMixin, AfterLayoutMixin {\n  late double _textFactorDouble;\n  double _offset = 1;\n  late TextScaler _textFactor;\n\n  final _cardsStatus = <String, _CardNotifier>{};\n  late final ValueNotifier<Set<String>> _tags;\n\n  Timer? _timer;\n\n  final _tag = ''.vn;\n\n  final _scrollController = ScrollController();\n  final _autoHideCtrl = AutoHideController();\n\n  @override\n  void dispose() {\n    super.dispose();\n    _timer?.cancel();\n    _scrollController.dispose();\n    _autoHideCtrl.dispose();\n    _tag.dispose();\n    _tags.dispose();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _tags = ValueNotifier(ref.read(serversProvider).tags);\n    _startAvoidJitterTimer();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    _updateOffset();\n  }\n\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    // Listen to provider changes and update the ValueNotifier\n    ref.listen(serversProvider, (previous, next) {\n      _tags.value = next.tags;\n    });\n    return OrientationBuilder(\n      builder: (_, orientation) {\n        if (orientation == Orientation.landscape) {\n          final useFullScreen = Stores.setting.fullScreen.fetch();\n          // Only enter landscape mode when the screen is wide enough and the\n          // full screen mode is enabled.\n          if (useFullScreen) return _buildLandscape();\n        }\n        return _buildPortrait();\n      },\n    );\n  }\n\n  Widget _buildScaffold(Widget child) {\n    return Scaffold(\n      appBar: _TopBar(tags: _tags, onTagChanged: (p0) => _tag.value = p0, initTag: _tag.value),\n      body: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: _autoHideCtrl.show,\n        child: Stores.setting.textFactor.listenable().listenVal((val) {\n          _updateTextScaler(val);\n          return child;\n        }),\n      ),\n      floatingActionButton: AutoHide(\n        direction: AxisDirection.right,\n        offset: 75,\n        scrollController: _scrollController,\n        hideController: _autoHideCtrl,\n        child: FloatingActionButton(\n          heroTag: 'addServer',\n          onPressed: _onTapAddServer,\n          tooltip: libL10n.add,\n          child: const Icon(Icons.add),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildPortrait() {\n    // final isMobile = ResponsiveBreakpoints.of(context).isMobile;\n    final serverState = ref.watch(serversProvider);\n    return _tag.listenVal((val) {\n      final filtered = _filterServers(serverState.serverOrder);\n      final child = _buildScaffold(_buildBodySmall(filtered: filtered));\n      // if (isMobile) {\n      return child;\n      // }\n\n      // return SplitView(\n      //   controller: _splitViewCtrl,\n      //   leftWeight: 1,\n      //   rightWeight: 1.3,\n      //   initialRight: Center(child: CircularProgressIndicator()),\n      //   leftBuilder: (_, __) => child,\n      // );\n    });\n  }\n\n  Widget _buildBodySmall({required List<String> filtered}) {\n    if (filtered.isEmpty) {\n      return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));\n    }\n\n    return LayoutBuilder(\n      builder: (_, cons) {\n        // Calculate number of columns based on available width\n        final columnsCount = math.max(1, (cons.maxWidth / UIs.columnWidth).floor());\n        final padding = columnsCount > 1\n            ? const EdgeInsets.fromLTRB(0, 0, 5, 7)\n            : const EdgeInsets.fromLTRB(7, 0, 7, 7);\n\n        return Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: List.generate(columnsCount, (colIndex) {\n            // Calculate which servers belong in this column\n            final serversInThisColumn = <String>[];\n            for (int i = colIndex; i < filtered.length; i += columnsCount) {\n              serversInThisColumn.add(filtered[i]);\n            }\n            final lens = serversInThisColumn.length;\n\n            return Expanded(\n              child: ListView.builder(\n                controller: colIndex == 0 ? _scrollController : null,\n                padding: padding,\n                itemCount: lens + 1, // Add 1 for bottom spacing\n                itemBuilder: (context, index) {\n                  // Last item is just spacing\n                  if (index == lens) return SizedBox(height: 77);\n\n                  final individualState = ref.watch(serverProvider(serversInThisColumn[index]));\n\n                  return _buildEachServerCard(individualState);\n                },\n              ),\n            );\n          }),\n        );\n      },\n    );\n  }\n\n  Widget _buildEachServerCard(ServerState srv) {\n    return CardX(\n      key: Key(srv.spi.id + _tag.value),\n      child: InkWell(\n        onTap: () => _onTapCard(srv),\n        onLongPress: () => _onLongPressCard(srv),\n        child: Padding(\n          padding: const EdgeInsets.only(\n            left: _cardPadSingle,\n            right: 3,\n            top: _cardPadSingle,\n            bottom: _cardPadSingle,\n          ),\n          child: _buildRealServerCard(srv),\n        ),\n      ),\n    );\n  }\n\n  /// The child's width mat not equal to 1/4 of the screen width,\n  /// so we need to wrap it with a SizedBox.\n  Widget _wrapWithSizedbox(Widget child, double maxWidth, [bool circle = false]) {\n    return LayoutBuilder(\n      builder: (_, cons) {\n        final width = (maxWidth - _cardPad) / 4;\n        return SizedBox(width: width, child: child);\n      },\n    );\n  }\n\n  Widget _buildRealServerCard(ServerState srv) {\n    final id = srv.spi.id;\n    final cardStatus = _getCardNoti(id);\n    final title = _buildServerCardTitle(srv);\n\n    return cardStatus.listenVal((_) {\n      final List<Widget> children = [title];\n      if (srv.conn == ServerConn.finished) {\n        if (cardStatus.value.flip) {\n          children.add(_buildFlippedCard(srv));\n        } else {\n          children.add(_buildNormalCard(srv.status, srv.spi));\n        }\n      }\n\n      final height = _calcCardHeight(srv.conn, cardStatus.value.flip);\n      return AnimatedContainer(\n        duration: const Duration(milliseconds: 377),\n        curve: Curves.fastEaseInToSlowEaseOut,\n        height: height,\n        // Use [OverflowBox] to dismiss the warning of [Column] overflow.\n        child: OverflowBox(\n          // If `height == _kCardHeightMin`, the `maxHeight` will be ignored.\n          //\n          // You can comment the `maxHeight` then connect&disconnect the server\n          // to see the difference.\n          maxHeight: height != _kCardHeightMin ? height : null,\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.center,\n            children: children,\n          ),\n        ),\n      );\n    });\n  }\n\n  Widget _buildFlippedCard(ServerState srv) {\n    const color = Colors.grey;\n    const textStyle = TextStyle(fontSize: 13, color: color);\n    final children = [\n      Btn.column(\n        onTap: () => _onTapSuspend(srv),\n        icon: const Icon(Icons.stop, color: color),\n        text: libL10n.suspend,\n        textStyle: textStyle,\n      ),\n      Btn.column(\n        onTap: () => _onTapShutdown(srv),\n        icon: const Icon(Icons.power_off, color: color),\n        text: libL10n.shutdown,\n        textStyle: textStyle,\n      ),\n      Btn.column(\n        onTap: () => _onTapReboot(srv),\n        icon: const Icon(Icons.restart_alt, color: color),\n        text: libL10n.reboot,\n        textStyle: textStyle,\n      ),\n      Btn.column(\n        onTap: () => _onTapEdit(srv),\n        icon: const Icon(Icons.edit, color: color),\n        text: libL10n.edit,\n        textStyle: textStyle,\n      ),\n    ];\n\n    return Padding(\n      padding: const EdgeInsets.only(top: 9),\n      child: LayoutBuilder(\n        builder: (_, cons) {\n          final width = (cons.maxWidth - _cardPad) / children.length;\n          return Row(\n            mainAxisAlignment: MainAxisAlignment.spaceAround,\n            children: children.map((e) {\n              if (width == 0) return e;\n              return SizedBox(width: width, child: e);\n            }).toList(),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildNormalCard(ServerStatus ss, Spi spi) {\n    return LayoutBuilder(\n      builder: (_, cons) {\n        final maxWidth = cons.maxWidth;\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            UIs.height13,\n            Row(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                _wrapWithSizedbox(PercentCircle(percent: ss.cpu.usedPercent()), maxWidth, true),\n                _wrapWithSizedbox(PercentCircle(percent: ss.mem.usedPercent * 100), maxWidth, true),\n                _wrapWithSizedbox(_buildNet(ss, spi.id), maxWidth),\n                _wrapWithSizedbox(_buildDisk(ss, spi.id), maxWidth),\n              ],\n            ),\n            UIs.height13,\n            if (Stores.setting.moveServerFuncs.fetch()) SizedBox(height: 27, child: ServerFuncBtns(spi: spi)),\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Future<void> afterFirstLayout(BuildContext context) async {\n    ref.read(serversProvider.notifier).refresh();\n    ref.read(serversProvider.notifier).startAutoRefresh();\n  }\n\n  static const _kCardHeightMin = 23.0;\n  static const _kCardHeightFlip = 99.0;\n  static const _kCardHeightNormal = 110.0;\n  static const _kCardHeightMoveOutFuncs = 135.0;\n}\n"
  },
  {
    "path": "lib/view/page/server/tab/top_bar.dart",
    "content": "part of 'tab.dart';\n\nfinal class _TopBar extends ConsumerWidget implements PreferredSizeWidget {\n  final ValueNotifier<Set<String>> tags;\n  final void Function(String) onTagChanged;\n  final String initTag;\n\n  const _TopBar({required this.initTag, required this.onTagChanged, required this.tags});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final breakpoints = ResponsiveBreakpoints.of(context);\n    final isMobile = breakpoints.isMobile;\n    final padding = EdgeInsets.only(left: isMobile ? 10 : 16, right: isMobile ? 0 : 16);\n\n    final Widget leading;\n    if (isMobile) {\n      // Keep this btn. For issue #657.\n      leading = InkWell(\n        borderRadius: BorderRadius.circular(13),\n        onTap: () {\n          SettingsPage.route.go(context);\n        },\n        child: const Padding(\n          padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3),\n          child: Row(\n            children: [\n              Text(BuildData.name, style: TextStyle(fontSize: 19)),\n              SizedBox(width: 5),\n              Icon(Icons.settings, size: 17),\n            ],\n          ),\n        ),\n      );\n    } else {\n      final servers = ref.watch(serversProvider);\n      final order = servers.serverOrder;\n      var connected = 0;\n      for (final id in order) {\n        final conn = ref.watch(serverProvider(id).select((value) => value.conn));\n        if (conn.index >= ServerConn.connected.index) connected++;\n      }\n      final total = order.length;\n      final connectionText = '$connected/$total ${context.libL10n.conn}';\n      leading = Text(connectionText, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600));\n    }\n\n    return Padding(\n      padding: padding,\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          leading,\n          SizedBox(width: isMobile ? 30 : 16),\n          TagSwitcher(\n            tags: tags,\n            onTagChanged: onTagChanged,\n            initTag: initTag,\n            singleLine: true,\n            reversed: true,\n          ).expanded(),\n        ],\n      ),\n    );\n  }\n\n  @override\n  Size get preferredSize => const Size.fromHeight(TagSwitcher.kTagBtnHeight);\n}\n"
  },
  {
    "path": "lib/view/page/server/tab/utils.dart",
    "content": "// ignore_for_file: invalid_use_of_protected_member\n\npart of 'tab.dart';\n\nextension _Actions on _ServerPageState {\n  void _onTapCard(ServerState srv) {\n    if (srv.canViewDetails) {\n      // _splitViewCtrl.replace(ServerDetailPage(\n      //   key: ValueKey(srv.spi.id),\n      //   args: SpiRequiredArgs(srv.spi),\n      // ));\n      ServerDetailPage.route.go(context, SpiRequiredArgs(srv.spi));\n    } else {\n      // _splitViewCtrl.replace(ServerEditPage(\n      //   key: ValueKey(srv.spi.id),\n      //   args: SpiRequiredArgs(srv.spi),\n      // ));\n      ServerEditPage.route.go(context, args: SpiRequiredArgs(srv.spi));\n    }\n  }\n\n  void _onLongPressCard(ServerState srv) {\n    if (srv.conn == ServerConn.finished) {\n      final id = srv.spi.id;\n      final cardStatus = _getCardNoti(id);\n      cardStatus.value = cardStatus.value.copyWith(flip: !cardStatus.value.flip);\n    } else {\n      ServerEditPage.route.go(context, args: SpiRequiredArgs(srv.spi));\n    }\n  }\n\n  void _onTapAddServer() {\n    //   final isMobile = ResponsiveBreakpoints.of(context).isMobile;\n    //   if (isMobile) {\n    ServerEditPage.route.go(context);\n    //   } else {\n    //     _splitViewCtrl.replace(const ServerEditPage(\n    //       key: ValueKey('addServer'),\n    //     ));\n    //   }\n  }\n}\n\nextension _Operation on _ServerPageState {\n  void _onTapSuspend(ServerState srv) {\n    _askFor(\n      func: () async {\n        if (Stores.setting.showSuspendTip.fetch()) {\n          await context.showRoundDialog(title: libL10n.attention, child: Text(l10n.suspendTip));\n          Stores.setting.showSuspendTip.put(false);\n        }\n        await srv.client?.execWithPwd(\n          ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system, customDir: null),\n          context: context,\n          id: srv.id,\n        ) ??\n            (null, '');\n      },\n      typ: libL10n.suspend,\n      name: srv.spi.name,\n    );\n  }\n\n  void _onTapShutdown(ServerState srv) {\n    _askFor(\n      func: () async {\n        await srv.client?.execWithPwd(\n          ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system, customDir: null),\n          context: context,\n          id: srv.id,\n        );\n      },\n      typ: libL10n.shutdown,\n      name: srv.spi.name,\n    );\n  }\n\n  void _onTapReboot(ServerState srv) {\n    _askFor(\n      func: () async {\n        await srv.client?.execWithPwd(\n          ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system, customDir: null),\n          context: context,\n          id: srv.id,\n        ) ??\n            (null, '');\n      },\n      typ: libL10n.reboot,\n      name: srv.spi.name,\n    );\n  }\n\n  void _onTapEdit(ServerState srv) {\n    if (srv.canViewDetails) {\n      ServerDetailPage.route.go(context, SpiRequiredArgs(srv.spi));\n    } else {\n      ServerEditPage.route.go(context, args: SpiRequiredArgs(srv.spi));\n    }\n  }\n}\n\nextension _Utils on _ServerPageState {\n  List<String> _filterServers(List<String> order) {\n    final tag = _tag.value;\n    if (tag == TagSwitcher.kDefaultTag) return order;\n    return order.where((e) {\n      final tags = ref.read(serversProvider).servers[e]?.tags;\n      if (tags == null) return false;\n      return tags.contains(tag);\n    }).toList();\n  }\n\n  double? _calcCardHeight(ServerConn cs, bool flip) {\n    if (_textFactorDouble != 1.0) return null;\n    if (cs != ServerConn.finished) {\n      return _ServerPageState._kCardHeightMin;\n    }\n    if (flip) {\n      return _ServerPageState._kCardHeightFlip;\n    }\n    if (Stores.setting.moveServerFuncs.fetch()) {\n      return _ServerPageState._kCardHeightMoveOutFuncs;\n    }\n    return _ServerPageState._kCardHeightNormal;\n  }\n\n  void _askFor({required void Function() func, required String typ, required String name}) {\n    context.showRoundDialog(\n      title: libL10n.attention,\n      child: Text(libL10n.askContinue('$typ ${libL10n.server}($name)')),\n      actions: Btn.ok(\n        onTap: () {\n          context.pop();\n          func();\n        },\n      ).toList,\n    );\n  }\n\n  _CardNotifier _getCardNoti(String id) =>\n      _cardsStatus.putIfAbsent(id, () => _CardNotifier(const _CardStatus()));\n\n  void _updateOffset() {\n    if (!Stores.setting.fullScreenJitter.fetch()) return;\n    final x = MediaQuery.sizeOf(context).height * 0.03;\n    final r = math.Random().nextDouble();\n    final n = math.Random().nextBool() ? 1 : -1;\n    _offset = x * r * n;\n  }\n\n  void _updateTextScaler(double val) {\n    _textFactorDouble = val;\n    _textFactor = TextScaler.linear(_textFactorDouble);\n  }\n\n  void _startAvoidJitterTimer() {\n    if (!Stores.setting.fullScreenJitter.fetch()) return;\n    _timer = Timer.periodic(const Duration(seconds: 30), (_) {\n      if (mounted) {\n        _updateOffset();\n        setState(() {});\n      } else {\n        _timer?.cancel();\n      }\n    });\n  }\n}\n\nextension _ServerX on ServerState {\n  String? _getTopRightStr(Spi spi) {\n    if (status.err != null) {\n      return l10n.viewErr;\n    }\n    switch (conn) {\n      case ServerConn.disconnected:\n        return null;\n      case ServerConn.finished:\n        // Highest priority of temperature display\n        final cmdTemp = () {\n          final val = status.customCmds['server_card_top_right'];\n          if (val == null) return null;\n          // This returned value is used on server card top right, so it should\n          // be a single line string.\n          return val.split('\\n').lastOrNull;\n        }();\n        final temperatureVal = () {\n          // Second priority\n          final preferTempDev = spi.custom?.preferTempDev;\n          if (preferTempDev != null) {\n            final preferTemp = status.sensors\n                .firstWhereOrNull((e) => e.device == preferTempDev)\n                ?.summary\n                ?.split(' ')\n                .firstOrNull;\n            if (preferTemp != null) {\n              return double.tryParse(preferTemp.replaceFirst('°C', ''));\n            }\n          }\n          // Last priority\n          final temp = status.temps.first;\n          if (temp != null) {\n            return temp;\n          }\n          return null;\n        }();\n        final upTime = status.more[StatusCmdType.uptime];\n        final items = [\n          cmdTemp ?? (temperatureVal != null ? '${temperatureVal.toStringAsFixed(1)}°C' : null),\n          upTime,\n        ];\n        final str = items.where((e) => e != null && e.isNotEmpty).join(' | ');\n        if (str.isEmpty) return libL10n.empty;\n        return str;\n      case ServerConn.loading:\n        return null;\n      case ServerConn.connected:\n        return null;\n      case ServerConn.connecting:\n        return null;\n      case ServerConn.failed:\n        return libL10n.fail;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/about.dart",
    "content": "part of 'entry.dart';\n\nfinal class _AppAboutPage extends StatefulWidget {\n  const _AppAboutPage();\n\n  @override\n  State<_AppAboutPage> createState() => _AppAboutPageState();\n}\n\nfinal class _AppAboutPageState extends State<_AppAboutPage> with AutomaticKeepAliveClientMixin {\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return SafeArea(\n      child: ListView(\n        padding: const EdgeInsets.all(13),\n        children: [\n        UIs.height13,\n        ConstrainedBox(constraints: const BoxConstraints(maxHeight: 47, maxWidth: 47), child: UIs.appIcon),\n        const Text('${BuildData.name}\\nv${BuildData.build}', textAlign: TextAlign.center, style: UIs.text15),\n        UIs.height13,\n        SizedBox(\n          height: 77,\n          child: ListView(\n            padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 7),\n            scrollDirection: Axis.horizontal,\n            children: <Widget>[\n              Btn.elevated(\n                icon: const Icon(Icons.edit_document),\n                text: 'Wiki',\n                onTap: Urls.appWiki.launchUrl,\n              ),\n              Btn.elevated(\n                icon: const Icon(Icons.feedback),\n                text: libL10n.feedback,\n                onTap: Urls.appHelp.launchUrl,\n              ),\n              Btn.elevated(\n                icon: const Icon(MingCute.question_fill),\n                text: libL10n.license,\n                onTap: () => showLicensePage(context: context),\n              ),\n            ].joinWith(UIs.width13),\n          ),\n        ),\n        UIs.height13,\n        SimpleMarkdown(\n          data:\n              '''\n#### Contributors\n${GithubIds.contributors.map((e) => '[$e](${e.url})').join(' ')}\n\n#### Participants\n${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}\n\n#### My other apps\n[GPT Box](https://github.com/lollipopkit/flutter_gpt_box)\n\n${l10n.madeWithLove('[lollipopkit](${Urls.myGithub})')}\n''',\n        ).paddingAll(13).cardx,\n      ],\n    ),\n    );\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n}\n"
  },
  {
    "path": "lib/view/page/setting/entries/ai.dart",
    "content": "part of '../entry.dart';\n\nextension _AI on _AppSettingsPageState {\n  Widget _buildAskAiConfig() {\n    final l10n = context.l10n;\n    return ExpandTile(\n      leading: const Icon(LineAwesome.robot_solid, size: _kIconSize),\n      title: TipText(l10n.askAi, l10n.askAiUsageHint),\n      children: [\n        _setting.askAiBaseUrl.listenable().listenVal((val) {\n          final display = val.isEmpty ? libL10n.empty : val;\n          return ListTile(\n            leading: const Icon(MingCute.link_2_line),\n            title: Text(l10n.askAiBaseUrl),\n            subtitle: Text(display, style: UIs.textGrey, maxLines: 2, overflow: TextOverflow.ellipsis),\n            onTap: () => _showAskAiFieldDialog(\n              prop: _setting.askAiBaseUrl,\n              title: l10n.askAiBaseUrl,\n              hint: 'https://api.openai.com',\n            ),\n          );\n        }),\n        _setting.askAiModel.listenable().listenVal((val) {\n          final display = val.isEmpty ? libL10n.empty : val;\n          return ListTile(\n            leading: const Icon(Icons.view_module),\n            title: Text(libL10n.askAiModel),\n            subtitle: Text(display, style: UIs.textGrey),\n            onTap: () => _showAskAiFieldDialog(\n              prop: _setting.askAiModel,\n              title: libL10n.askAiModel,\n              hint: 'gpt-4o-mini',\n            ),\n          );\n        }),\n        _setting.askAiApiKey.listenable().listenVal((val) {\n          final hasKey = val.isNotEmpty;\n          return ListTile(\n            leading: const Icon(MingCute.key_2_line),\n            title: Text(l10n.askAiApiKey),\n            subtitle: Text(hasKey ? '••••••••' : libL10n.empty, style: UIs.textGrey),\n            onTap: () => _showAskAiFieldDialog(\n              prop: _setting.askAiApiKey,\n              title: l10n.askAiApiKey,\n              hint: 'sk-...',\n              obscure: true,\n            ),\n          );\n        }),\n      ],\n    ).cardx;\n  }\n\n\n  Future<void> _showAskAiFieldDialog({\n    required HiveProp<String> prop,\n    required String title,\n    required String hint,\n    bool obscure = false,\n  }) async {\n    return withTextFieldController((ctrl) async {\n      final fetched = prop.fetch();\n      if (fetched != null && fetched.isNotEmpty) ctrl.text = fetched;\n\n      void onSave() {\n        prop.put(ctrl.text.trim());\n        context.pop();\n      }\n\n      await context.showRoundDialog(\n        title: title,\n        child: Input(\n          controller: ctrl,\n          autoFocus: true,\n          label: title,\n          hint: hint,\n          icon: obscure ? MingCute.key_2_line : Icons.edit,\n          obscureText: obscure,\n          suggestion: !obscure,\n          onSubmitted: (_) => onSave(),\n        ),\n        actions: [\n          TextButton(\n            onPressed: () {\n              prop.delete();\n              context.pop();\n            },\n            child: Text(libL10n.clear),\n          ),\n          TextButton(onPressed: onSave, child: Text(libL10n.ok)),\n        ],\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/entries/app.dart",
    "content": "part of '../entry.dart';\n\nextension _App on _AppSettingsPageState {\n  Widget _buildApp() {\n    final androidSettings = isAndroid ? _buildAndroidSettings() : null;\n    final specific = _buildPlatformSetting();\n    final children = [\n      _buildLocale(),\n      _buildThemeMode(),\n      _buildAppColor(),\n      _buildCheckUpdate(),\n      _buildHomeTabs(),\n      PlatformPublicSettings.buildBioAuth,\n      ?androidSettings,\n      ?specific,\n      _buildAppMore(),\n    ];\n\n    return Column(children: children.map((e) => e.cardx).toList());\n  }\n\n  Widget _buildAndroidSettings() {\n    return ExpandTile(\n      leading: const Icon(Icons.phone_android),\n      title: Text('Android ${libL10n.setting}'),\n      children: [\n        _buildBgRun(),\n        _buildAndroidWidgetSharedPreference(),\n      ],\n    );\n  }\n\n  Widget _buildBgRun() {\n    return ListTile(\n      title: TipText(l10n.bgRun, l10n.bgRunTip),\n      trailing: StoreSwitch(prop: Stores.setting.bgRun),\n    );\n  }\n\n  Widget _buildAndroidWidgetSharedPreference() {\n    return ListTile(\n      title: Text(l10n.homeWidgetUrlConfig),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () async {\n        const prefix = 'widget_';\n        final data = <String, String>{};\n        final keys = PrefStore.shared.keys();\n\n        for (final key in keys) {\n          if (!key.startsWith(prefix)) continue;\n          final val = PrefStore.shared.get<String>(key);\n          if (val != null) data[key] = val;\n        }\n        final result = await KvEditor.route.go(\n          context,\n          KvEditorArgs(data: data, prefix: prefix),\n        );\n        if (result != null) {\n          await _saveWidgetSP(result, data, prefix);\n        }\n      },\n    );\n  }\n\n  Future<void> _saveWidgetSP(Map<String, String> map, Map<String, String> old, String prefix) async {\n    try {\n      final keysDel = old.keys.toSet().difference(map.keys.toSet());\n      for (final key in keysDel) {\n        if (!key.startsWith(prefix)) continue;\n        await PrefStore.shared.remove(key);\n      }\n      for (final entry in map.entries) {\n        if (!entry.key.startsWith(prefix)) continue;\n        await PrefStore.shared.set(entry.key, entry.value);\n      }\n      if (mounted) context.showSnackBar(libL10n.success);\n    } catch (e) {\n      if (mounted) context.showSnackBar(e.toString());\n    }\n  }\n\n  Widget? _buildPlatformSetting() {\n    if (!isIOS) return null;\n    return ListTile(\n      leading: const Icon(MingCute.apple_fill),\n      title: Text('iOS ${libL10n.setting}'),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () => IosSettingsPage.route.go(context),\n    );\n  }\n\n  Widget _buildCheckUpdate() {\n    return ListTile(\n      leading: const Icon(Icons.update),\n      title: Text(libL10n.checkUpdate),\n      subtitle: ValBuilder(\n        listenable: AppUpdateIface.newestBuild,\n        builder: (val) {\n          String display;\n          if (val != null) {\n            if (val > BuildData.build) {\n              display = libL10n.versionHasUpdate(val);\n            } else {\n              display = libL10n.versionUpdated(BuildData.build);\n            }\n          } else {\n            display = libL10n.versionUnknownUpdate(BuildData.build);\n          }\n          return Text(display, style: UIs.textGrey);\n        },\n      ),\n      onTap: () => Fns.throttle(\n        () => AppUpdateIface.doUpdate(\n          context: context,\n          build: BuildData.build,\n          url: Urls.updateCfg,\n          force: BuildMode.isDebug,\n        ),\n      ),\n      trailing: StoreSwitch(prop: _setting.autoCheckAppUpdate),\n    );\n  }\n\n  Widget _buildUpdateInterval() {\n    return ListTile(\n      title: Text(l10n.updateServerStatusInterval),\n      onTap: () async {\n        final val = await context.showPickSingleDialog(\n          title: libL10n.setting,\n          items: List.generate(10, (idx) => idx == 1 ? null : idx),\n          initial: _setting.serverStatusUpdateInterval.fetch(),\n          display: (p0) => p0 == 0 ? libL10n.manual : '$p0 ${l10n.second}',\n        );\n        if (val != null) {\n          _setting.serverStatusUpdateInterval.put(val);\n        }\n      },\n      trailing: ValBuilder(\n        listenable: _setting.serverStatusUpdateInterval.listenable(),\n        builder: (val) => Text('$val ${l10n.second}', style: UIs.text15),\n      ),\n    );\n  }\n\n  Widget _buildAppColor() {\n    return ListTile(\n      leading: const Icon(Icons.colorize),\n      title: Text(libL10n.primaryColorSeed),\n      trailing: _setting.colorSeed.listenable().listenVal((_) {\n        return ClipOval(child: Container(color: UIs.primaryColor, height: 27, width: 27));\n      }),\n      onTap: () {\n        withTextFieldController((ctrl) async {\n          ctrl.text = Color(_setting.colorSeed.fetch()).toHex;\n          await context.showRoundDialog(\n            title: libL10n.primaryColorSeed,\n            child: StatefulBuilder(\n              builder: (context, setState) {\n                final children = <Widget>[\n                  if (!isIOS)\n                    DynamicColorBuilder(\n                      builder: (light, dark) {\n                        final supported = light != null || dark != null;\n                        if (!supported) {\n                          if (!_setting.useSystemPrimaryColor.fetch()) {\n                            _setting.useSystemPrimaryColor.put(false);\n                            WidgetsBinding.instance.addPostFrameCallback((_) {\n                              setState(() {});\n                            });\n                          }\n                          return const SizedBox.shrink();\n                        }\n                        return ListTile(\n                          title: Text(l10n.followSystem),\n                          trailing: StoreSwitch(\n                            prop: _setting.useSystemPrimaryColor,\n                            callback: (_) => setState(() {}),\n                          ),\n                        );\n                      },\n                    ),\n                ];\n                if (!_setting.useSystemPrimaryColor.fetch()) {\n                  children.add(\n                    ColorPicker(\n                      color: Color(_setting.colorSeed.fetch()),\n                      onColorChanged: (c) => ctrl.text = c.toHex,\n                    ),\n                  );\n                }\n                return Column(mainAxisSize: MainAxisSize.min, children: children);\n              },\n            ),\n            actions: [\n              Btn.cancel(),\n              Btn.ok(onTap: () => _onSaveColor(ctrl.text)),\n            ],\n          );\n        });\n      },\n    );\n  }\n\n  void _onSaveColor(String s) {\n    final color = s.fromColorHex;\n\n    if (color == null) {\n      context.showSnackBar(libL10n.fail);\n      return;\n    }\n\n    // Save the color seed to settings\n    _setting.colorSeed.put(color.value255);\n\n    // Only update UIs colors if we're not in system mode\n    if (!_setting.useSystemPrimaryColor.fetch()) {\n      UIs.primaryColor = color;\n      UIs.colorSeed = color;\n    }\n\n    RNodes.app.notify();\n    context.pop();\n  }\n\n  Widget _buildMaxRetry() {\n    return ValBuilder(\n      listenable: _setting.maxRetryCount.listenable(),\n      builder: (val) => ListTile(\n        title: Text(l10n.maxRetryCount),\n        onTap: () async {\n          final selected = await context.showPickSingleDialog(\n            title: l10n.maxRetryCount,\n            items: List.generate(10, (index) => index),\n            display: (p0) => '$p0 ${l10n.times}',\n            initial: val,\n          );\n          if (selected != null) {\n            _setting.maxRetryCount.put(selected);\n          }\n        },\n        trailing: Text('$val ${l10n.times}', style: UIs.text15),\n      ),\n    );\n  }\n\n  Widget _buildThemeMode() {\n    // Issue #57\n    final len = ThemeMode.values.length;\n    return ListTile(\n      leading: const Icon(MingCute.moon_stars_fill),\n      title: Text(libL10n.themeMode),\n      onTap: () async {\n        final selected = await context.showPickSingleDialog(\n          title: libL10n.themeMode,\n          items: List.generate(len + 2, (index) => index),\n          display: (p0) => _buildThemeModeStr(p0),\n          initial: _setting.themeMode.fetch(),\n        );\n        if (selected != null) {\n          _setting.themeMode.put(selected);\n          RNodes.app.notify();\n        }\n      },\n      trailing: ValBuilder(\n        listenable: _setting.themeMode.listenable(),\n        builder: (val) => Text(_buildThemeModeStr(val), style: UIs.text15),\n      ),\n    );\n  }\n\n  String _buildThemeModeStr(int n) {\n    switch (n) {\n      case 1:\n        return libL10n.bright;\n      case 2:\n        return libL10n.dark;\n      case 3:\n        return 'AMOLED';\n      case 4:\n        return '${libL10n.auto} AMOLED';\n      default:\n        return libL10n.auto;\n    }\n  }\n\n  Widget _buildLocale() {\n    return ListTile(\n      leading: const Icon(IonIcons.language),\n      title: Text(libL10n.language),\n      onTap: () async {\n        final selected = await context.showPickSingleDialog(\n          title: libL10n.language,\n          items: AppLocalizations.supportedLocales,\n          display: (p0) => p0.nativeName,\n          initial: _setting.locale.fetch().toLocale,\n        );\n        if (selected != null) {\n          _setting.locale.put(selected.code);\n          context.pop();\n          RNodes.app.notify();\n        }\n      },\n      trailing: ListenBuilder(\n        listenable: _setting.locale.listenable(),\n        builder: () => Text(context.localeNativeName, style: UIs.text15),\n      ),\n    );\n  }\n\n  Widget _buildAppMore() {\n    return ExpandTile(\n      leading: const Icon(MingCute.more_3_fill),\n      title: Text(l10n.more),\n      initiallyExpanded: false,\n      children: [\n        _buildBeta(),\n        if (isMobile) _buildWakeLock(),\n        _buildCollapseUI(),\n        if (isDesktop) _buildHideTitleBar(),\n        _buildEditRawSettings(),\n      ],\n    );\n  }\n\n  Widget _buildBeta() {\n    return ListTile(\n      title: TipText('Beta Program', l10n.acceptBeta),\n      trailing: StoreSwitch(prop: _setting.betaTest),\n    );\n  }\n\n  Widget _buildWakeLock() {\n    return ListTile(\n      title: Text(l10n.wakeLock),\n      trailing: StoreSwitch(prop: _setting.generalWakeLock),\n    );\n  }\n\n  Widget _buildCollapseUI() {\n    return ListTile(\n      title: TipText('UI ${libL10n.fold}', l10n.collapseUITip),\n      trailing: StoreSwitch(prop: _setting.collapseUIDefault),\n    );\n  }\n\n  Widget _buildHideTitleBar() {\n    return ListTile(\n      title: Text(l10n.hideTitleBar),\n      trailing: StoreSwitch(prop: _setting.hideTitleBar),\n    );\n  }\n\n  Widget _buildHomeTabs() {\n    return ListTile(\n      leading: const Icon(Icons.tab),\n      title: Text(l10n.homeTabs),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () {\n        HomeTabsConfigPage.route.go(context);\n      },\n    );\n  }\n\n  Widget _buildEditRawSettings() {\n    return ListTile(\n      title: const Text('(Dev) Edit raw json'),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: _editRawSettings,\n    );\n  }\n\n  Future<void> _editRawSettings() async {\n    final rawMap = Stores.setting.getAllMap(includeInternalKeys: true);\n    final map = Map<String, Object?>.from(rawMap);\n    final initialKeys = Set<String>.from(map.keys);\n    Map<String, Object?> mapForEditor = map;\n    String? encryptedKey;\n    String? passwordUsed;\n\n    Future<String?> resolvePassword() async {\n      final saved = await _setting.backupasswd.read();\n      if (saved?.isNotEmpty == true) return saved;\n      final backupPwd = await SecureStoreProps.bakPwd.read();\n      if (backupPwd?.isNotEmpty == true) return backupPwd;\n      final controller = TextEditingController();\n      try {\n        final result = await context.showRoundDialog<String>(\n          title: libL10n.pwd,\n          child: Input(\n            controller: controller,\n            label: libL10n.pwd,\n            obscureText: true,\n            onSubmitted: (_) => context.pop(controller.text.trim()),\n          ),\n          actions: [\n            TextButton(onPressed: () => context.pop(null), child: Text(libL10n.cancel)),\n            TextButton(onPressed: () => context.pop(controller.text.trim()), child: Text(libL10n.ok)),\n          ],\n        );\n        return result?.trim();\n      } finally {\n        controller.dispose();\n      }\n    }\n\n    for (final entry in map.entries) {\n      final value = entry.value;\n      if (value is String && Cryptor.isEncrypted(value)) {\n        final password = await resolvePassword();\n        if (password == null || password.isEmpty) {\n          context.showSnackBar(libL10n.cancel);\n          return;\n        }\n        try {\n          final decrypted = Cryptor.decrypt(value, password);\n          final decoded = json.decode(decrypted);\n          if (decoded is Map<String, dynamic>) {\n            mapForEditor = Map<String, Object?>.from(decoded);\n            encryptedKey = entry.key;\n            passwordUsed = password;\n            break;\n          } else {\n            context.showRoundDialog(title: libL10n.fail, child: Text(l10n.invalid));\n            return;\n          }\n        } catch (e, stack) {\n          final msg = e.toString().contains('Failed to decrypt') || e.toString().contains('incorrect password')\n              ? l10n.backupPasswordWrong\n              : '${libL10n.error}:\\n$e';\n          context.showRoundDialog(title: libL10n.fail, child: Text(msg));\n          Loggers.app.warning('Decrypt raw settings failed', e, stack);\n          return;\n        }\n      }\n    }\n\n    void onSave(EditorPageRet ret) {\n      if (ret.typ != EditorPageRetType.text) {\n        context.showRoundDialog(title: libL10n.fail, child: Text(l10n.invalid));\n        return;\n      }\n      try {\n        final newSettings = json.decode(ret.val) as Map<String, dynamic>;\n        if (encryptedKey != null) {\n          final pwd = passwordUsed;\n          if (pwd == null || pwd.isEmpty) {\n            context.showRoundDialog(title: libL10n.fail, child: Text(l10n.invalid));\n            return;\n          }\n          final encrypted = Cryptor.encrypt(json.encode(newSettings), pwd);\n          Stores.setting.box.put(encryptedKey, encrypted);\n        } else {\n          Stores.setting.box.putAll(newSettings);\n          final newKeys = newSettings.keys.toSet();\n          final removedKeys = initialKeys.where((e) => !newKeys.contains(e));\n          for (final key in removedKeys) {\n            Stores.setting.box.delete(key);\n          }\n        }\n      } catch (e, trace) {\n        context.showRoundDialog(title: libL10n.error, child: Text('${l10n.save}:\\n$e'));\n        Loggers.app.warning('Update json settings failed', e, trace);\n      }\n    }\n\n    /// Encode [map] to String with indent `\\t`\n    final text = jsonIndentEncoder.convert(mapForEditor);\n    final editorFont = _setting.editorFontFamily.fetch();\n    await EditorPage.route.go(\n      context,\n      args: EditorPageArgs(\n        text: text,\n        lang: ProgLang.json,\n        title: libL10n.setting,\n        onSave: onSave,\n        closeAfterSave: _setting.closeAfterSave.fetch(),\n        softWrap: _setting.editorSoftWrap.fetch(),\n        enableHighlight: _setting.editorHighlight.fetch(),\n        fontFamily: editorFont.isEmpty ? null : editorFont,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/entries/container.dart",
    "content": "part of '../entry.dart';\n\nextension _Container on _AppSettingsPageState {\n  Widget _buildContainer() {\n    return Column(\n      children: [\n        _buildUsePodman(),\n        _buildContainerTrySudo(),\n        _buildContainerParseStat(),\n      ].map((e) => CardX(child: e)).toList(),\n    );\n  }\n\n  Widget _buildUsePodman() {\n    return ListTile(\n      leading: const Icon(IonIcons.logo_docker),\n      title: Text(l10n.usePodmanByDefault),\n      trailing: StoreSwitch(prop: _setting.usePodman),\n    );\n  }\n\n  Widget _buildContainerTrySudo() {\n    return ListTile(\n      leading: const Icon(EvaIcons.person_done),\n      title: TipText(l10n.trySudo, l10n.containerTrySudoTip),\n      trailing: StoreSwitch(prop: _setting.containerTrySudo),\n    );\n  }\n\n  Widget _buildContainerParseStat() {\n    return ListTile(\n      leading: const Icon(MingCute.chart_line_line, size: _kIconSize),\n      // title: Text(l10n.parseContainerStats),\n      // subtitle: Text(l10n.parseContainerStatsTip, style: UIs.textGrey),\n      title: TipText(l10n.stat, l10n.parseContainerStatsTip),\n      trailing: StoreSwitch(prop: _setting.containerParseStat),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/entries/editor.dart",
    "content": "part of '../entry.dart';\n\nextension _Editor on _AppSettingsPageState {\n  Widget _buildEditor() {\n    return Column(\n      children: [\n        _buildEditorWrap(),\n        _buildEditorFontFamily(),\n        _buildEditorFontSize(),\n        _buildEditorTheme(),\n        _buildEditorDarkTheme(),\n        _buildEditorHighlight(),\n        _buildEditorCloseAfterEdit(),\n      ].map((e) => CardX(child: e)).toList(),\n    );\n  }\n\n  Widget _buildEditorCloseAfterEdit() {\n    return ListTile(\n      leading: const Icon(MingCute.edit_fill),\n      title: Text(l10n.closeAfterSave),\n      trailing: StoreSwitch(prop: _setting.closeAfterSave),\n    );\n  }\n\n  Widget _buildEditorHighlight() {\n    return ListTile(\n      leading: const Icon(MingCute.code_line, size: _kIconSize),\n      // title: Text(l10n.highlight),\n      // subtitle: Text(l10n.editorHighlightTip, style: UIs.textGrey),\n      title: TipText(l10n.highlight, l10n.editorHighlightTip),\n      trailing: StoreSwitch(prop: _setting.editorHighlight),\n    );\n  }\n\n  Widget _buildEditorTheme() {\n    return ListTile(\n      leading: const Icon(MingCute.sun_fill),\n      title: Text('${libL10n.bright} ${libL10n.theme.toLowerCase()}'),\n      trailing: ValBuilder(\n        listenable: _setting.editorTheme.listenable(),\n        builder: (val) => Text(val, style: UIs.text15),\n      ),\n      onTap: () async {\n        final selected = await context.showPickSingleDialog(\n          title: libL10n.theme,\n          items: themeMap.keys.toList(),\n          display: (p0) => p0,\n          initial: _setting.editorTheme.fetch(),\n        );\n        if (selected != null) {\n          _setting.editorTheme.put(selected);\n        }\n      },\n    );\n  }\n\n  Widget _buildEditorDarkTheme() {\n    return ListTile(\n      leading: const Icon(MingCute.moon_stars_fill),\n      title: Text('${libL10n.dark} ${libL10n.theme.toLowerCase()}'),\n      trailing: ValBuilder(\n        listenable: _setting.editorDarkTheme.listenable(),\n        builder: (val) => Text(val, style: UIs.text15),\n      ),\n      onTap: () async {\n        final selected = await context.showPickSingleDialog(\n          title: libL10n.theme,\n          items: themeMap.keys.toList(),\n          display: (p0) => p0,\n          initial: _setting.editorDarkTheme.fetch(),\n        );\n        if (selected != null) {\n          _setting.editorDarkTheme.put(selected);\n        }\n      },\n    );\n  }\n\n  Widget _buildEditorWrap() {\n    return ListTile(\n      leading: const Icon(MingCute.align_center_line),\n      title: Text(l10n.softWrap),\n      trailing: StoreSwitch(prop: _setting.editorSoftWrap),\n    );\n  }\n\n  Widget _buildEditorFontSize() {\n    return ListTile(\n      leading: const Icon(MingCute.font_size_line),\n      title: Text(l10n.fontSize),\n      trailing: ValBuilder(\n        listenable: _setting.editorFontSize.listenable(),\n        builder: (val) => Text(val.toString(), style: UIs.text15),\n      ),\n      onTap: () => _showFontSizeDialog(_setting.editorFontSize),\n    );\n  }\n\n  Widget _buildEditorFontFamily() {\n    return ListTile(\n      leading: const Icon(MingCute.font_fill),\n      title: Text(libL10n.font),\n      trailing: ValBuilder(\n        listenable: _setting.editorFontFamily.listenable(),\n        builder: (val) => Text(\n          val.isEmpty ? libL10n.auto.toLowerCase() : val,\n          style: UIs.text15,\n        ),\n      ),\n      onTap: () => _showFontFamilyDialog(_setting.editorFontFamily),\n    );\n  }\n\n  void _showFontFamilyDialog(HiveProp<String> property) {\n    final ctrl = TextEditingController(text: property.fetch());\n    void onSave() {\n      context.pop();\n      property.put(ctrl.text.trim());\n    }\n\n    context.showRoundDialog(\n      title: libL10n.font,\n      child: Input(\n        controller: ctrl,\n        autoFocus: true,\n        type: TextInputType.text,\n        icon: Icons.font_download,\n        hint: 'monospace / Consolas / Fira Code ...',\n        suggestion: false,\n        onSubmitted: (_) => onSave(),\n      ),\n      actions: Btn.ok(onTap: onSave).toList,\n    );\n  }\n\n  void _showFontSizeDialog(HiveProp<double> property) {\n    final ctrl = TextEditingController(text: property.fetch().toString());\n    void onSave() {\n      context.pop();\n      final fontSize = double.tryParse(ctrl.text);\n      if (fontSize == null) {\n        context.showRoundDialog(\n          title: libL10n.fail,\n          child: Text('Parsed failed: ${ctrl.text}'),\n        );\n        return;\n      }\n      property.set(fontSize);\n    }\n\n    context.showRoundDialog(\n      title: l10n.fontSize,\n      child: Input(\n        controller: ctrl,\n        autoFocus: true,\n        type: TextInputType.number,\n        icon: Icons.font_download,\n        suggestion: false,\n        onSubmitted: (_) => onSave(),\n      ),\n      actions: Btn.ok(onTap: onSave).toList,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/entries/full_screen.dart",
    "content": "part of '../entry.dart';\n\nextension _Fullscreen on _AppSettingsPageState {\n  Widget _buildFullScreen() {\n    return Column(\n      children: [\n        _buildFullScreenSwitch(),\n        _buildFullScreenJitter(),\n      ].map((e) => CardX(child: e)).toList(),\n    );\n  }\n\n  Widget _buildFullScreenSwitch() {\n    return ListTile(\n      leading: const Icon(Bootstrap.phone_landscape_fill),\n      // title: Text(l10n.fullScreen),\n      // subtitle: Text(l10n.fullScreenTip, style: UIs.textGrey),\n      title: TipText(l10n.fullScreen, l10n.fullScreenTip),\n      trailing: StoreSwitch(\n        prop: _setting.fullScreen,\n        callback: (_) => RNodes.app.notify(),\n      ),\n    );\n  }\n\n  Widget _buildFullScreenJitter() {\n    return ListTile(\n      leading: const Icon(AntDesign.shake_outline),\n      title: Text(l10n.fullScreenJitter),\n      subtitle: Text(l10n.fullScreenJitterHelp, style: UIs.textGrey),\n      trailing: StoreSwitch(\n        prop: _setting.fullScreenJitter,\n        callback: (_) {\n          context.showSnackBar(l10n.needRestart);\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/entries/home_tabs.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/app/tab.dart';\nimport 'package:server_box/data/res/store.dart';\n\nclass HomeTabsConfigPage extends ConsumerStatefulWidget {\n  const HomeTabsConfigPage({super.key});\n\n  static final route = AppRouteNoArg(page: HomeTabsConfigPage.new, path: '/settings/home-tabs');\n\n  @override\n  ConsumerState<HomeTabsConfigPage> createState() => _HomeTabsConfigPageState();\n}\n\nclass _HomeTabsConfigPageState extends ConsumerState<HomeTabsConfigPage> {\n  final _availableTabs = AppTab.values;\n  var _selectedTabs = List<AppTab>.from(Stores.setting.homeTabs.fetch());\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(\n        title: Text(l10n.homeTabs),\n        actions: [\n          TextButton(onPressed: _resetToDefault, child: Text(libL10n.reset)),\n          TextButton(onPressed: _saveAndExit, child: Text(libL10n.save)),\n        ],\n      ),\n      body: Column(\n        children: [\n          Padding(\n            padding: const EdgeInsets.all(16),\n            child: Text(l10n.homeTabsCustomizeDesc, style: context.theme.textTheme.bodyMedium),\n          ),\n          Expanded(\n            child: ReorderableListView.builder(\n              itemCount: _selectedTabs.length,\n              onReorder: _onReorder,\n              buildDefaultDragHandles: false,\n              itemBuilder: (context, index) {\n                final tab = _selectedTabs[index];\n                return _buildTabItem(tab, index, true);\n              },\n            ),\n          ),\n          const Divider(),\n          Padding(\n            padding: const EdgeInsets.all(16),\n            child: Text(l10n.availableTabs, style: context.theme.textTheme.titleMedium),\n          ),\n          Expanded(\n            child: ListView.builder(\n              itemCount: _availableTabs.length,\n              itemBuilder: (context, index) {\n                final tab = _availableTabs[index];\n                if (_selectedTabs.contains(tab)) {\n                  return const SizedBox.shrink();\n                }\n                return _buildTabItem(tab, index, false);\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildTabItem(AppTab tab, int index, bool isSelected) {\n    final canRemove = _selectedTabs.length > 1 && tab != AppTab.server;\n    final child = ListTile(\n      leading: tab.navDestination.icon,\n      title: Text(tab.navDestination.label),\n      trailing: isSelected\n          ? IconButton(\n              icon: const Icon(Icons.delete),\n              onPressed: canRemove ? () => _removeTab(tab) : null,\n              color: canRemove ? null : Theme.of(context).disabledColor,\n              tooltip: canRemove ? libL10n.delete : (tab == AppTab.server ? l10n.serverTabRequired : l10n.atLeastOneTab),\n            )\n          : IconButton(icon: const Icon(Icons.add), onPressed: () => _addTab(tab)),\n      onTap: isSelected && canRemove ? () => _removeTab(tab) : null,\n    );\n\n    return Padding(\n      key: ValueKey(tab.name),\n      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),\n      child: (isSelected ? ReorderableDragStartListener(index: index, child: child) : child).cardx,\n    );\n  }\n\n  void _onReorder(int oldIndex, int newIndex) {\n    setState(() {\n      if (newIndex > oldIndex) {\n        newIndex -= 1;\n      }\n      final tab = _selectedTabs.removeAt(oldIndex);\n      _selectedTabs.insert(newIndex, tab);\n    });\n  }\n\n  void _addTab(AppTab tab) {\n    setState(() {\n      _selectedTabs.add(tab);\n    });\n  }\n\n  void _removeTab(AppTab tab) {\n    if (_selectedTabs.length <= 1) {\n      context.showSnackBar(l10n.atLeastOneTab);\n      return;\n    }\n    if (tab == AppTab.server) {\n      context.showSnackBar(l10n.serverTabRequired);\n      return;\n    }\n    setState(() {\n      _selectedTabs.remove(tab);\n    });\n  }\n\n  void _saveAndExit() {\n    Stores.setting.homeTabs.put(_selectedTabs);\n    context.pop();\n  }\n\n  void _resetToDefault() {\n    setState(() {\n      _selectedTabs = List<AppTab>.from(AppTab.values);\n    });\n    Stores.setting.homeTabs.put(_selectedTabs);\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/entries/server.dart",
    "content": "part of '../entry.dart';\n\nextension _Server on _AppSettingsPageState {\n  Widget _buildServer() {\n    return Column(\n      children: [\n        _buildServerLogoUrl(),\n        _buildServerFuncBtns(),\n        _buildNetViewType(),\n        _buildServerSeq(),\n        _buildServerDetailCardSeq(),\n        _buildDeleteServers(),\n        _buildCpuView(),\n        _buildServerMore(),\n      ].map((e) => CardX(child: e)).toList(),\n    );\n  }\n\n  Widget _buildNetViewType() {\n    return ListTile(\n      leading: const Icon(ZondIcons.network, size: _kIconSize),\n      title: Text(l10n.netViewType),\n      trailing: ValBuilder(\n        listenable: _setting.netViewType.listenable(),\n        builder: (val) => Text(val.toStr, style: UIs.text15),\n      ),\n      onTap: () async {\n        final selected = await context.showPickSingleDialog(\n          title: l10n.netViewType,\n          items: NetViewType.values,\n          display: (p0) => p0.toStr,\n          initial: _setting.netViewType.fetch(),\n        );\n        if (selected != null) {\n          _setting.netViewType.put(selected);\n        }\n      },\n    );\n  }\n\n  Widget _buildDeleteServers() {\n    return ListTile(\n      title: Text(l10n.deleteServers),\n      leading: const Icon(Icons.delete_forever),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () async {\n        final keys = Stores.server.keys();\n        final names = Map.fromEntries(\n          keys.map((e) => MapEntry(e, ref.read(serversProvider).servers[e]?.name ?? e)),\n        );\n        final deleteKeys = await context.showPickDialog<String>(\n          clearable: true,\n          items: keys.toList(),\n          display: (p0) => names[p0] ?? p0,\n        );\n        if (deleteKeys == null || deleteKeys.isEmpty) return;\n\n        final md = deleteKeys.map((e) => '- ${names[e] ?? e}').join('\\n');\n        final sure = await context.showRoundDialog(\n          title: libL10n.attention,\n          child: SimpleMarkdown(data: md),\n          actions: Btnx.cancelRedOk,\n        );\n\n        if (sure != true) return;\n        for (final key in deleteKeys) {\n          Stores.server.remove(key);\n        }\n        context.showSnackBar(libL10n.success);\n      },\n    );\n  }\n\n  Widget _buildTextScaler() {\n    return ListTile(\n      // title: Text(l10n.textScaler),\n      // subtitle: Text(l10n.textScalerTip, style: UIs.textGrey),\n      title: TipText(l10n.textScaler, l10n.textScalerTip),\n      trailing: ValBuilder(\n        listenable: _setting.textFactor.listenable(),\n        builder: (val) => Text(val.toString(), style: UIs.text15),\n      ),\n      onTap: () => context.showRoundDialog(\n        title: l10n.textScaler,\n        child: Input(\n          autoFocus: true,\n          type: TextInputType.number,\n          hint: '1.0',\n          icon: Icons.format_size,\n          controller: _textScalerCtrl,\n          onSubmitted: _onSaveTextScaler,\n          suggestion: false,\n        ),\n        actions: Btn.ok(onTap: () => _onSaveTextScaler(_textScalerCtrl.text)).toList,\n      ),\n    );\n  }\n\n  void _onSaveTextScaler(String s) {\n    final val = double.tryParse(s);\n    if (val == null) {\n      context.showSnackBar(libL10n.fail);\n      return;\n    }\n    _setting.textFactor.put(val);\n    RNodes.app.notify();\n    context.pop();\n  }\n\n  Widget _buildServerFuncBtns() {\n    return ExpandTile(\n      leading: const Icon(BoxIcons.bxs_joystick_button, size: _kIconSize),\n      title: Text(l10n.serverFuncBtns),\n      children: [_buildServerFuncBtnsSwitch(), _buildServerFuncBtnsOrder()],\n    );\n  }\n\n  Widget _buildServerFuncBtnsSwitch() {\n    return ListTile(\n      // title: Text(libL10n.location),\n      // subtitle: Text(l10n.moveOutServerFuncBtnsHelp, style: UIs.text13Grey),\n      title: TipText(libL10n.location, l10n.moveOutServerFuncBtnsHelp),\n      trailing: StoreSwitch(prop: _setting.moveServerFuncs),\n    );\n  }\n\n  Widget _buildServerFuncBtnsOrder() {\n    return ListTile(\n      title: Text(libL10n.sequence),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () => ServerFuncBtnsOrderPage.route.go(context),\n    );\n  }\n\n  Widget _buildServerSeq() {\n    return ListTile(\n      leading: const Icon(OctIcons.sort_desc, size: _kIconSize),\n      title: Text(l10n.serverOrder),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () => ServerOrderPage.route.go(context),\n    );\n  }\n\n  Widget _buildServerDetailCardSeq() {\n    return ListTile(\n      leading: const Icon(OctIcons.sort_desc, size: _kIconSize),\n      title: Text(l10n.serverDetailOrder),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () => ServerDetailOrderPage.route.go(context),\n    );\n  }\n\n  Widget _buildDoubleColumnServersPage() {\n    return ListTile(\n      // title: Text(l10n.doubleColumnMode),\n      // subtitle: Text(l10n.doubleColumnTip, style: UIs.textGrey),\n      title: TipText(l10n.doubleColumnMode, l10n.doubleColumnTip),\n      trailing: StoreSwitch(prop: _setting.doubleColumnServersPage),\n    );\n  }\n\n  Widget _buildKeepStatusWhenErr() {\n    return ListTile(\n      title: Text(l10n.keepStatusWhenErr),\n      subtitle: Text(l10n.keepStatusWhenErrTip, style: UIs.textGrey),\n      trailing: StoreSwitch(prop: _setting.keepStatusWhenErr),\n    );\n  }\n\n  Widget _buildServerMore() {\n    return ExpandTile(\n      leading: const Icon(MingCute.more_3_fill),\n      title: Text(l10n.more),\n      initiallyExpanded: false,\n      children: [\n        _buildServerTabPreferDiskAmount(),\n        _buildRememberPwdInMem(),\n        _buildTextScaler(),\n        _buildKeepStatusWhenErr(),\n        _buildDoubleColumnServersPage(),\n        _buildUpdateInterval(),\n        _buildMaxRetry(),\n        if (isDesktop) _buildSSHConfigAutoImportToggle(),\n      ],\n    );\n  }\n\n  Widget _buildRememberPwdInMem() {\n    return ListTile(\n      // title: Text(l10n.rememberPwdInMem),\n      // subtitle: Text(l10n.rememberPwdInMemTip, style: UIs.textGrey),\n      title: TipText(l10n.rememberPwdInMem, l10n.rememberPwdInMemTip),\n      trailing: StoreSwitch(prop: _setting.rememberPwdInMem),\n    );\n  }\n\n  Widget _buildCpuView() {\n    return ExpandTile(\n      leading: const Icon(OctIcons.cpu, size: _kIconSize),\n      title: Text('CPU ${l10n.view}'),\n      children: [\n        ListTile(\n          title: Text(l10n.noLineChart),\n          subtitle: Text(l10n.cpuViewAsProgressTip, style: UIs.textGrey),\n          trailing: StoreSwitch(prop: _setting.cpuViewAsProgress),\n        ),\n        ListTile(\n          title: Text(l10n.displayCpuIndex),\n          trailing: StoreSwitch(prop: _setting.displayCpuIndex),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildServerLogoUrl() {\n    void onSave(String url) {\n      if (url.isEmpty || !url.startsWith('http')) {\n        context.showRoundDialog(title: libL10n.fail, child: Text('${l10n.invalid} URL'), actions: Btnx.oks);\n        return;\n      }\n      _setting.serverLogoUrl.put(url);\n      context.pop();\n    }\n\n    return ListTile(\n      leading: const Icon(Icons.image),\n      title: const Text('Logo URL'),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () {\n        context.showRoundDialog(\n          title: 'Logo URL',\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              Input(\n                controller: _serverLogoCtrl,\n                autoFocus: true,\n                hint: 'https://example.com/logo.png',\n                icon: Icons.link,\n                maxLines: 2,\n                suggestion: false,\n                onSubmitted: onSave,\n              ),\n              ListTile(\n                title: Text(libL10n.doc),\n                trailing: const Icon(Icons.open_in_new),\n                onTap: Urls.appWiki.launchUrl,\n              ),\n            ],\n          ),\n          actions: Btn.ok(onTap: () => onSave(_serverLogoCtrl.text)).toList,\n        );\n      },\n    );\n  }\n\n  Widget _buildServerTabPreferDiskAmount() {\n    return ListTile(\n      title: Text(l10n.preferDiskAmount),\n      trailing: StoreSwitch(prop: Stores.setting.serverTabPreferDiskAmount),\n    );\n  }\n\n  Widget _buildSSHConfigAutoImportToggle() {\n    return ListTile(\n      title: Text(l10n.sshConfigImport),\n      subtitle: Text(l10n.sshConfigImportTip, style: UIs.textGrey),\n      trailing: StoreSwitch(prop: _setting.firstTimeReadSSHCfg),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/entries/sftp.dart",
    "content": "part of '../entry.dart';\n\nextension _SFTP on _AppSettingsPageState {\n  Widget _buildSFTP() {\n    return Column(\n      children: [\n        _buildSftpEditor(),\n        _buildSftpRmrDir(),\n        _buildSftpOpenLastPath(),\n        _buildSftpShowFoldersFirst(),\n      ].map((e) => CardX(child: e)).toList(),\n    );\n  }\n\n  Widget _buildSftpOpenLastPath() {\n    return ListTile(\n      leading: const Icon(MingCute.history_line),\n      // title: Text(l10n.openLastPath),\n      // subtitle: Text(l10n.openLastPathTip, style: UIs.textGrey),\n      title: TipText(l10n.openLastPath, l10n.openLastPathTip),\n      trailing: StoreSwitch(prop: _setting.sftpOpenLastPath),\n    );\n  }\n\n  Widget _buildSftpShowFoldersFirst() {\n    return ListTile(\n      leading: const Icon(MingCute.folder_fill),\n      title: Text(l10n.sftpShowFoldersFirst),\n      trailing: StoreSwitch(prop: _setting.sftpShowFoldersFirst),\n    );\n  }\n\n  Widget _buildSftpRmrDir() {\n    return ListTile(\n      leading: const Icon(MingCute.delete_2_fill),\n      title: TipText('rm -r', l10n.sftpRmrDirSummary),\n      trailing: StoreSwitch(prop: _setting.sftpRmrDir),\n    );\n  }\n\n  Widget _buildSftpEditor() {\n    return _setting.sftpEditor.listenable().listenVal((val) {\n      return ListTile(\n        leading: const Icon(MingCute.edit_fill),\n        title: TipText(libL10n.editor, l10n.sftpEditorTip),\n        trailing: Text(val.isEmpty ? libL10n.inner : val, style: UIs.text15),\n        onTap: () {\n          withTextFieldController((ctrl) async {\n            void onSave() {\n              final s = ctrl.text.trim();\n              _setting.sftpEditor.put(s);\n              context.pop();\n            }\n\n            await context.showRoundDialog<bool>(\n              title: libL10n.select,\n              child: Input(\n                controller: ctrl,\n                autoFocus: true,\n                label: libL10n.editor,\n                hint: '\\$EDITOR / vim / nano ...',\n                icon: Icons.edit,\n                suggestion: false,\n                onSubmitted: (_) => onSave(),\n              ),\n              actions: Btn.ok(onTap: onSave).toList,\n            );\n          });\n        },\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/entries/ssh.dart",
    "content": "part of '../entry.dart';\n\nextension _SSH on _AppSettingsPageState {\n  Widget _buildSSH() {\n    return Column(\n      children: [\n        if (isDesktop) _buildSSHConfigImport(),\n        if (isMobile) _buildQrScan(),\n        _buildSSHDiscovery(),\n        _buildLetterCache(),\n        _buildSSHWakeLock(),\n        _buildTermTheme(),\n        _buildFont(),\n        _buildTermFontSize(),\n        _buildSshBg(),\n        if (isDesktop) _buildDesktopTerminal(),\n        _buildSSHVirtualKeyAutoOff(),\n        if (isMobile) _buildSSHVirtKeys(),\n      ].map((e) => CardX(child: e)).toList(),\n    );\n  }\n\n  Widget _buildSSHConfigImport() {\n    return ListTile(\n      leading: const Icon(MingCute.file_import_line),\n      title: Text(l10n.sshConfigImport),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: _onTapSSHConfigImport,\n    );\n  }\n\n  Widget _buildQrScan() {\n    return ListTile(\n      leading: const Icon(Icons.qr_code),\n      title: Text(libL10n.import),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: _onTapQrScan,\n    );\n  }\n\n  Future<void> _onTapQrScan() async {\n    final ret = await BarcodeScannerPage.route.go(\n      context,\n      args: const BarcodeScannerPageArgs(),\n    );\n    final code = ret?.text;\n    if (code == null) return;\n    if (!mounted) return;\n\n    try {\n      final spi = Spi.fromJson(json.decode(code));\n      final existingIds = ref.read(serversProvider).servers.keys;\n      if (existingIds.contains(spi.id)) {\n        context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');\n        return;\n      }\n      final resolvedList = ServerDeduplication.resolveNameConflicts([spi]);\n      final resolvedSpi = resolvedList.first;\n      ref.read(serversProvider.notifier).addServer(resolvedSpi);\n      context.showSnackBar(libL10n.success);\n    } catch (e, s) {\n      context.showErrDialog(e, s);\n    }\n  }\n\n  Widget _buildSSHDiscovery() {\n    return ListTile(\n      leading: const Icon(BoxIcons.bx_search),\n      title: Text(l10n.discoverSshServers),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: _onTapSSHDiscovery,\n    );\n  }\n\n  Future<void> _onTapSSHDiscovery() async {\n    try {\n      final result = await SshDiscoveryPage.route.go(context);\n      if (!mounted) return;\n\n      if (result != null && result.isNotEmpty) {\n        await _processDiscoveredServers(result);\n      }\n    } catch (e, s) {\n      if (!mounted) return;\n      context.showErrDialog(e, s);\n    }\n  }\n\n  Future<void> _processDiscoveredServers(\n    List<SshDiscoveryResult> discoveredServers,\n  ) async {\n    final defaultUsername = 'root';\n    final usernameController = TextEditingController(text: defaultUsername);\n\n    try {\n      final shouldImport = await context.showRoundDialog<bool>(\n        title: libL10n.import,\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            Text(l10n.sshConfigFoundServers('${discoveredServers.length}')),\n            const SizedBox(height: 8),\n            Input(\n              controller: usernameController,\n              label: libL10n.user,\n            ),\n          ],\n        ),\n        actions: Btnx.cancelOk,\n      );\n\n      if (!mounted) return;\n\n      if (shouldImport == true) {\n        final username = usernameController.text.isNotEmpty\n            ? usernameController.text\n            : defaultUsername;\n        final servers = discoveredServers\n            .map(\n              (result) => Spi(\n                id: ShortId.generate(),\n                name: result.ip,\n                ip: result.ip,\n                port: result.port,\n                user: username,\n              ),\n            )\n            .toList();\n\n        await ServerDeduplication.importServersWithNotification(\n          servers: servers,\n          ref: ref,\n          context: context,\n          allExistMessage: l10n.sshConfigAllExist,\n          importedMessage: (count) => '${libL10n.success}: $count ${libL10n.servers}',\n        );\n      }\n    } finally {\n      usernameController.dispose();\n    }\n  }\n\n  Future<void> _onTapSSHConfigImport() async {\n    try {\n      final servers = await SSHConfig.parseConfig();\n      if (!mounted) return;\n      if (servers.isEmpty) {\n        context.showSnackBar(l10n.sshConfigNoServers);\n        return;\n      }\n\n      await _processSSHServers(servers);\n    } catch (e, s) {\n      if (!mounted) return;\n      await _handleImportSSHCfgPermissionIssue(e, s);\n    }\n  }\n\n  Future<void> _processSSHServers(List<Spi> servers) async {\n    final deduplicated = ServerDeduplication.deduplicateServers(servers);\n    final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);\n    final summary = ServerDeduplication.getImportSummary(servers, resolved);\n\n    if (!summary.hasItemsToImport) {\n      if (!mounted) return;\n      context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));\n      return;\n    }\n\n    final shouldImport = await context.showRoundDialog<bool>(\n      title: l10n.sshConfigImport,\n      child: SingleChildScrollView(\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(l10n.sshConfigFoundServers('${summary.total}')),\n            if (summary.hasDuplicates)\n              Text(\n                l10n.sshConfigDuplicatesSkipped('${summary.duplicates}'),\n                style: UIs.textGrey,\n              ),\n            Text(l10n.sshConfigServersToImport('${summary.toImport}')),\n            const SizedBox(height: 16),\n            ...resolved.map(\n              (s) => Text('• ${s.name} (${s.user}@${s.ip}:${s.port})'),\n            ),\n          ],\n        ),\n      ),\n      actions: Btnx.cancelOk,\n    );\n\n    if (!mounted) return;\n\n    if (shouldImport == true) {\n      await ServerDeduplication.importServersWithNotification(\n        ref: ref,\n        context: context,\n        resolvedServers: resolved,\n        originalCount: summary.total,\n        allExistMessage: l10n.sshConfigAllExist,\n        importedMessage: l10n.sshConfigImported,\n      );\n    }\n  }\n\n  Future<void> _handleImportSSHCfgPermissionIssue(Object e, StackTrace s) async {\n    dprint('Error importing SSH config: $e');\n    if (e is PathAccessException ||\n        e.toString().contains('Operation not permitted')) {\n      final useFilePicker = await context.showRoundDialog<bool>(\n        title: l10n.sshConfigImport,\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(l10n.sshConfigPermissionDenied),\n            const SizedBox(height: 8),\n            Text(l10n.sshConfigManualSelect),\n          ],\n        ),\n        actions: Btnx.cancelOk,\n      );\n\n      if (!mounted) return;\n\n      if (useFilePicker == true) {\n        await _onTapSSHImportWithFilePicker();\n      }\n    } else {\n      if (!mounted) return;\n      context.showErrDialog(e, s);\n    }\n  }\n\n  Future<void> _onTapSSHImportWithFilePicker() async {\n    try {\n      final result = await FilePicker.platform.pickFiles(\n        type: FileType.any,\n        allowMultiple: false,\n        dialogTitle: l10n.sshConfigImport,\n      );\n\n      if (!mounted) return;\n\n      if (result?.files.single.path case final path?) {\n        final servers = await SSHConfig.parseConfig(path);\n        if (!mounted) return;\n        if (servers.isEmpty) {\n          context.showSnackBar(l10n.sshConfigNoServers);\n          return;\n        }\n\n        await _processSSHServers(servers);\n      }\n    } catch (e, s) {\n      if (!mounted) return;\n      context.showErrDialog(e, s);\n    }\n  }\n\n  Widget _buildSSHVirtKeys() {\n    return ListTile(\n      leading: const Icon(BoxIcons.bxs_keyboard),\n      title: Text(l10n.editVirtKeys),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () => SSHVirtKeySettingPage.route.go(context),\n    );\n  }\n\n  Widget _buildSSHVirtualKeyAutoOff() {\n    return ListTile(\n      leading: const Icon(MingCute.hotkey_fill),\n      title: Text(l10n.sshVirtualKeyAutoOff),\n      subtitle: const Text('Ctrl & Alt', style: UIs.textGrey),\n      trailing: StoreSwitch(prop: _setting.sshVirtualKeyAutoOff),\n    );\n  }\n\n  Widget _buildFont() {\n    return ListTile(\n      leading: const Icon(MingCute.font_fill),\n      title: Text(libL10n.font),\n      trailing: _setting.fontPath.listenable().listenVal((val) {\n        final fontName = val.getFileName();\n        return Text(fontName ?? libL10n.empty, style: UIs.text15);\n      }),\n      onTap: () {\n        context.showRoundDialog(\n          title: libL10n.font,\n          actions: [\n            TextButton(onPressed: () async => await _pickFontFile(), child: Text(libL10n.file)),\n            TextButton(\n              onPressed: () {\n                _setting.fontPath.delete();\n                context.pop();\n                RNodes.app.notify();\n              },\n              child: Text(libL10n.clear),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  Future<void> _pickFontFile() async {\n    final path = await Pfs.pickFilePath();\n    if (path == null) return;\n\n    // iOS can't copy file to app dir, so we need to use the original path\n    if (isIOS) {\n      _setting.fontPath.put(path);\n      await FontUtils.loadFrom(path);\n    } else {\n      final fontFile = File(path);\n      await fontFile.copy(Paths.font);\n      _setting.fontPath.put(Paths.font);\n      await FontUtils.loadFrom(Paths.font);\n    }\n\n    context.pop();\n    RNodes.app.notify();\n  }\n\n  Widget _buildTermFontSize() {\n    return ListTile(\n      leading: const Icon(MingCute.font_size_line),\n      title: TipText(l10n.fontSize, l10n.termFontSizeTip),\n      trailing: ValBuilder(\n        listenable: _setting.termFontSize.listenable(),\n        builder: (val) => Text(val.toString(), style: UIs.text15),\n      ),\n      onTap: () => _showFontSizeDialog(_setting.termFontSize),\n    );\n  }\n\n  Future<void> _pickBgImage() async {\n    final path = await Pfs.pickFilePath();\n    if (path == null) return;\n\n    final file = File(path);\n    final extIndex = path.lastIndexOf('.');\n    final ext = extIndex != -1 ? path.substring(extIndex) : '';\n    final newPath = Paths.img.joinPath('ssh_bg$ext');\n    final destFile = File(newPath);\n    if (await destFile.exists()) {\n      await destFile.delete();\n    }\n    await file.copy(newPath);\n    _setting.sshBgImage.put(newPath);\n\n    context.pop();\n    RNodes.app.notify();\n  }\n\n  Widget _buildDesktopTerminal() {\n    return _setting.desktopTerminal.listenable().listenVal((val) {\n      return ListTile(\n        leading: const Icon(Icons.terminal),\n        title: TipText(libL10n.terminal, l10n.desktopTerminalTip),\n        trailing: Text(val, style: UIs.text15, maxLines: 1, overflow: TextOverflow.ellipsis),\n        onTap: () {\n          withTextFieldController((ctrl) async {\n            ctrl.text = val;\n            void onSave() {\n              _setting.desktopTerminal.put(ctrl.text.trim());\n              context.pop();\n            }\n\n            await context.showRoundDialog<bool>(\n              title: libL10n.select,\n              child: Input(\n                controller: ctrl,\n                autoFocus: true,\n                label: libL10n.terminal,\n                hint: 'x-terminal-emulator / gnome-terminal',\n                icon: Icons.edit,\n                suggestion: false,\n                onSubmitted: (_) => onSave(),\n              ),\n              actions: Btn.ok(onTap: onSave).toList,\n            );\n          });\n        },\n      );\n    });\n  }\n\n  Widget _buildTermTheme() {\n    String index2Str(int index) {\n      switch (index) {\n        case 0:\n          return l10n.system;\n        case 1:\n          return libL10n.bright;\n        case 2:\n          return libL10n.dark;\n        default:\n          return libL10n.error;\n      }\n    }\n\n    return ListTile(\n      leading: const Icon(MingCute.moon_stars_fill, size: _kIconSize),\n      title: Text(libL10n.theme),\n      trailing: ValBuilder(\n        listenable: _setting.termTheme.listenable(),\n        builder: (val) => Text(index2Str(val), style: UIs.text15),\n      ),\n      onTap: () async {\n        final selected = await context.showPickSingleDialog(\n          title: libL10n.theme,\n          items: List.generate(3, (index) => index),\n          display: (p0) => index2Str(p0),\n          initial: _setting.termTheme.fetch(),\n        );\n        if (selected != null) {\n          _setting.termTheme.put(selected);\n        }\n      },\n    );\n  }\n\n  Widget _buildSSHWakeLock() {\n    return ListTile(\n      leading: const Icon(MingCute.lock_fill),\n      title: Text(l10n.wakeLock),\n      trailing: StoreSwitch(prop: _setting.sshWakeLock),\n    );\n  }\n\n  Widget _buildLetterCache() {\n    return ListTile(\n      leading: const Icon(Bootstrap.alphabet),\n      // title: Text(l10n.letterCache),\n      // subtitle: Text(\n      //   '${l10n.letterCacheTip}\\n${l10n.needRestart}',\n      //   style: UIs.textGrey,\n      // ),\n      title: TipText(l10n.letterCache, '${l10n.letterCacheTip}\\n${l10n.needRestart}'),\n      trailing: StoreSwitch(prop: _setting.letterCache),\n    );\n  }\n\n  Widget _buildSshBg() {\n    return ExpandTile(\n      leading: const Icon(MingCute.background_fill),\n      title: Text(libL10n.background),\n      children: [_buildSshBgImage(), _buildSshBgOpacity(), _buildSshBlurRadius()],\n    );\n  }\n\n  Widget _buildSshBgImage() {\n    return ListTile(\n      leading: const Icon(Icons.image),\n      title: Text(libL10n.image),\n      trailing: _setting.sshBgImage.listenable().listenVal((val) {\n        final name = val.getFileName();\n        return Text(name ?? libL10n.empty, style: UIs.text15);\n      }),\n      onTap: () {\n        context.showRoundDialog(\n          title: libL10n.image,\n          actions: [\n            TextButton(onPressed: () async => await _pickBgImage(), child: Text(libL10n.file)),\n            TextButton(\n              onPressed: () {\n                _setting.sshBgImage.delete();\n                context.pop();\n                RNodes.app.notify();\n              },\n              child: Text(libL10n.clear),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget _buildSshBgOpacity() {\n    void onSave(String s) {\n      final val = double.tryParse(s);\n      if (val == null) {\n        context.showSnackBar(libL10n.fail);\n        return;\n      }\n      _setting.sshBgOpacity.put(val.clamp(0.0, 1.0));\n      context.pop();\n    }\n\n    return ListTile(\n      leading: const Icon(Icons.opacity),\n      title: Text(libL10n.opacity),\n      trailing: ValBuilder(\n        listenable: _setting.sshBgOpacity.listenable(),\n        builder: (val) => Text(val.toString(), style: UIs.text15),\n      ),\n      onTap: () => context.showRoundDialog(\n        title: libL10n.opacity,\n        child: Input(\n          controller: _sshOpacityCtrl,\n          autoFocus: true,\n          type: TextInputType.number,\n          hint: '0.3',\n          icon: Icons.opacity,\n          suggestion: false,\n          onSubmitted: onSave,\n        ),\n        actions: Btn.ok(onTap: () => onSave(_sshOpacityCtrl.text)).toList,\n      ),\n    );\n  }\n\n  Widget _buildSshBlurRadius() {\n    void onSave(String s) {\n      final val = double.tryParse(s);\n      if (val == null) {\n        context.showSnackBar(libL10n.fail);\n        return;\n      }\n      const minRadius = 0.0;\n      const maxBlur = 50.0;\n      final clampedVal = val.clamp(minRadius, maxBlur);\n      _setting.sshBlurRadius.put(clampedVal);\n      context.pop();\n    }\n\n    return ListTile(\n      leading: const Icon(Icons.blur_on),\n      title: Text(libL10n.blurRadius),\n      trailing: ValBuilder(\n        listenable: _setting.sshBlurRadius.listenable(),\n        builder: (val) => Text(val.toString(), style: UIs.text15),\n      ),\n      onTap: () => context.showRoundDialog(\n        title: libL10n.blurRadius,\n        child: Input(\n          controller: _sshBlurCtrl,\n          autoFocus: true,\n          type: TextInputType.number,\n          hint: '0',\n          icon: Icons.blur_on,\n          suggestion: false,\n          onSubmitted: onSave,\n        ),\n        actions: Btn.ok(onTap: () => onSave(_sshBlurCtrl.text)).toList,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/entry.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:dynamic_color/dynamic_color.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_highlight/theme_map.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/utils/server_dedup.dart';\nimport 'package:server_box/core/utils/ssh_config.dart';\nimport 'package:server_box/data/model/app/net_view.dart';\nimport 'package:server_box/data/model/server/discovery_result.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/res/build_data.dart';\nimport 'package:server_box/data/res/github_id.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/data/res/url.dart';\nimport 'package:server_box/data/store/setting.dart';\nimport 'package:server_box/generated/l10n/l10n.dart';\nimport 'package:server_box/view/page/backup.dart';\nimport 'package:server_box/view/page/private_key/list.dart';\nimport 'package:server_box/view/page/server/discovery/discovery.dart';\nimport 'package:server_box/view/page/setting/entries/home_tabs.dart';\nimport 'package:server_box/view/page/setting/platform/ios.dart';\nimport 'package:server_box/view/page/setting/platform/platform_pub.dart';\nimport 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';\nimport 'package:server_box/view/page/setting/seq/srv_func_seq.dart';\nimport 'package:server_box/view/page/setting/seq/srv_seq.dart';\nimport 'package:server_box/view/page/setting/seq/virt_key.dart';\n\npart 'about.dart';\npart 'entries/ai.dart';\npart 'entries/app.dart';\npart 'entries/container.dart';\npart 'entries/editor.dart';\npart 'entries/full_screen.dart';\npart 'entries/server.dart';\npart 'entries/sftp.dart';\npart 'entries/ssh.dart';\n\nconst _kIconSize = 23.0;\n\nclass SettingsPage extends ConsumerStatefulWidget {\n  const SettingsPage({super.key});\n\n  static const route = AppRouteNoArg(page: SettingsPage.new, path: '/settings');\n\n  @override\n  ConsumerState<SettingsPage> createState() => _SettingsPageState();\n}\n\nclass _SettingsPageState extends ConsumerState<SettingsPage> with SingleTickerProviderStateMixin {\n  late final _tabCtrl = TabController(length: SettingsTabs.values.length, vsync: this);\n\n  @override\n  void dispose() {\n    super.dispose();\n    _tabCtrl.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(\n        title: Text(libL10n.setting, style: const TextStyle(fontSize: 20)),\n        bottom: TabBar(\n          controller: _tabCtrl,\n          dividerHeight: 0,\n          tabAlignment: TabAlignment.center,\n          isScrollable: true,\n          tabs: SettingsTabs.values.map((e) => Tab(text: e.i18n)).toList(growable: false),\n        ),\n        actions: [\n          Btn.text(\n            text: context.libL10n.logs,\n            onTap: () =>\n                DebugPage.route.go(context, args: DebugPageArgs(title: '${context.libL10n.logs}(${BuildData.build})')),\n          ),\n          Btn.icon(\n            icon: const Icon(Icons.delete),\n            onTap: () => context.showRoundDialog(\n              title: libL10n.attention,\n              child: SimpleMarkdown(\n                data: libL10n.askContinue('${libL10n.delete} **${libL10n.all}** ${libL10n.setting}'),\n              ),\n              actions: [\n                CountDownBtn(\n                  onTap: () {\n                    context.pop();\n                    final keys = SettingStore.instance.box.keys;\n                    SettingStore.instance.box.deleteAll(keys);\n                    context.showSnackBar(libL10n.success);\n                  },\n                  afterColor: Colors.red,\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n      body: SafeArea(child: TabBarView(controller: _tabCtrl, children: SettingsTabs.pages)),\n    );\n  }\n}\n\nfinal class AppSettingsPage extends ConsumerStatefulWidget {\n  const AppSettingsPage({super.key});\n\n  @override\n  ConsumerState<AppSettingsPage> createState() => _AppSettingsPageState();\n}\n\nfinal class _AppSettingsPageState extends ConsumerState<AppSettingsPage> {\n  final _setting = Stores.setting;\n\n  late final _sshOpacityCtrl = TextEditingController(text: _setting.sshBgOpacity.fetch().toString());\n  late final _sshBlurCtrl = TextEditingController(text: _setting.sshBlurRadius.fetch().toString());\n  late final _textScalerCtrl = TextEditingController(text: _setting.textFactor.toString());\n  late final _serverLogoCtrl = TextEditingController(text: _setting.serverLogoUrl.fetch());\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiList(\n      children: [\n        [const CenterGreyTitle('App'), _buildApp(), const CenterGreyTitle('AI'), _buildAskAiConfig()],\n        [CenterGreyTitle(libL10n.server), _buildServer()],\n        [const CenterGreyTitle('SSH'), _buildSSH(), const CenterGreyTitle('SFTP'), _buildSFTP()],\n        [CenterGreyTitle(libL10n.container), _buildContainer(), CenterGreyTitle(libL10n.editor), _buildEditor()],\n\n        /// Fullscreen Mode is designed for old mobile phone which can be\n        /// used as a status screen.\n        if (isMobile) [CenterGreyTitle(l10n.fullScreen), _buildFullScreen()],\n      ],\n    );\n  }\n}\n\nenum SettingsTabs {\n  app,\n  privateKey,\n  backup,\n  about;\n\n  String get i18n => switch (this) {\n    SettingsTabs.app => libL10n.app,\n    SettingsTabs.privateKey => l10n.privateKey,\n    SettingsTabs.backup => libL10n.backup,\n    SettingsTabs.about => libL10n.about,\n  };\n\n  Widget get page => switch (this) {\n    SettingsTabs.app => const AppSettingsPage(),\n    SettingsTabs.privateKey => const PrivateKeysListPage(),\n    SettingsTabs.backup => const BackupPage(),\n    SettingsTabs.about => const _AppAboutPage(),\n  };\n\n  static final List<Widget> pages = SettingsTabs.values.map((e) => e.page).toList();\n}\n"
  },
  {
    "path": "lib/view/page/setting/platform/ios.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/utils/misc.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:watch_connectivity/watch_connectivity.dart';\n\nclass IosSettingsPage extends StatefulWidget {\n  const IosSettingsPage({super.key});\n\n  @override\n  State<IosSettingsPage> createState() => _IosSettingsPageState();\n\n  static const route = AppRouteNoArg(page: IosSettingsPage.new, path: '/settings/ios');\n}\n\nclass _IosSettingsPageState extends State<IosSettingsPage> {\n  final _pushToken = ValueNotifier<String?>(null);\n  final wc = WatchConnectivity();\n  late final _watchContextFuture = _loadWatchContext();\n  late final _pushTokenFuture = getToken();\n\n  Future<Map<String, dynamic>?> _loadWatchContext() async {\n    if (!await wc.isPaired) return null;\n    return await wc.applicationContext;\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    _pushToken.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(title: const Text('iOS')),\n      body: ListView(\n        padding: const EdgeInsets.symmetric(horizontal: 17),\n        children: [\n          _buildPushToken(),\n          _buildAutoUpdateHomeWidget(),\n          _buildWatchApp(),\n        ].map((e) => CardX(child: e)).toList(),\n      ),\n    );\n  }\n\n  Widget _buildPushToken() {\n    return ListTile(\n      title: Text(l10n.pushToken),\n      trailing: IconButton(\n        icon: const Icon(Icons.copy),\n        alignment: Alignment.centerRight,\n        padding: EdgeInsets.zero,\n        onPressed: () {\n          final val = _pushToken.value;\n          if (val != null) {\n            Pfs.copy(val);\n            context.showSnackBar(libL10n.success);\n          } else {\n            context.showSnackBar(libL10n.fail);\n          }\n        },\n      ),\n      subtitle: FutureWidget<String?>(\n        future: _pushTokenFuture,\n        loading: const Text('...'),\n        error: (error, trace) => Text('${libL10n.error}: $error'),\n        success: (text) {\n          _pushToken.value = text;\n          return Text(text ?? 'null', style: UIs.textGrey, overflow: TextOverflow.ellipsis, maxLines: 1);\n        },\n      ),\n    );\n  }\n\n  Widget _buildAutoUpdateHomeWidget() {\n    return ListTile(\n      title: Text(l10n.autoUpdateHomeWidget),\n      subtitle: Text(l10n.whenOpenApp, style: UIs.textGrey),\n      trailing: StoreSwitch(prop: Stores.setting.autoUpdateHomeWidget),\n    );\n  }\n\n  Widget _buildWatchApp() {\n    return FutureWidget(\n      future: _watchContextFuture,\n      loading: UIs.centerLoading,\n      error: (e, trace) {\n        Loggers.app.warning('WatchOS error', e, trace);\n        return ListTile(\n          title: const Text('Watch app'),\n          subtitle: Text('${libL10n.error}: $e', style: UIs.textGrey),\n        );\n      },\n      success: (ctx) {\n        if (ctx == null) {\n          return ListTile(\n            title: const Text('Watch app'),\n            subtitle: Text(l10n.watchNotPaired, style: UIs.textGrey),\n          );\n        }\n        return ListTile(\n          title: const Text('Watch app'),\n          trailing: const Icon(Icons.keyboard_arrow_right),\n          onTap: () async => _onTapWatchApp(ctx),\n        );\n      },\n    );\n  }\n\n  void _onTapWatchApp(Map<String, dynamic> map) async {\n    final cfgs = List<String>.from(map['urls'] as List? ?? []);\n    final result = await JsonListEditor.route.go(context, JsonListEditorArgs(data: cfgs));\n    if (result == null) return;\n\n    final (_, err) = await context.showLoadingDialog(\n      fn: () async {\n        final data = {'urls': result};\n        // Try realtime update (app must be running foreground).\n        try {\n          if (await wc.isReachable) {\n            await wc.sendMessage(data);\n            return;\n          }\n        } catch (e) {\n          Loggers.app.warning('Failed to send message to watch', e);\n        }\n\n        // fallback\n        await wc.updateApplicationContext(data);\n      },\n    );\n    if (err == null) {\n      context.showSnackBar(libL10n.success);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/platform/platform_pub.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/data/res/store.dart';\n\nabstract final class PlatformPublicSettings {\n  static Widget get buildBioAuth {\n    return ExpandTile(\n      leading: const Icon(Icons.fingerprint),\n      title: Text(libL10n.bioAuth),\n      children: [_buildBioAuth(), _buildBioAuthDelay()],\n    );\n  }\n\n  static Widget _buildBioAuthDelay() {\n    return FutureWidget<bool>(\n      future: LocalAuth.isAvail,\n      loading: ListTile(\n        title: Text('${libL10n.delay} (${libL10n.second})'),\n        subtitle: const Text('...', style: UIs.textGrey),\n      ),\n      error: (e, _) => ListTile(\n        title: Text('${libL10n.delay} (${libL10n.second})'),\n        subtitle: Text('${libL10n.fail}: $e', style: UIs.textGrey),\n      ),\n      success: (can) {\n        return ListTile(\n          title: Text('${libL10n.delay} (${libL10n.second})'),\n          trailing: can == true ? Stores.setting.delayBioAuthLock.fieldWidget() : null,\n        );\n      },\n    );\n  }\n\n  static Widget _buildBioAuth() {\n    return FutureWidget<bool>(\n      future: LocalAuth.isAvail,\n      loading: ListTile(\n        title: Text(libL10n.switch_),\n        subtitle: const Text('...', style: UIs.textGrey),\n      ),\n      error: (e, _) => ListTile(\n        title: Text(libL10n.switch_),\n        subtitle: Text('${libL10n.fail}: $e', style: UIs.textGrey),\n      ),\n      success: (can) {\n        can ??= false;\n        return ListTile(\n          title: Text(libL10n.switch_),\n          subtitle: can ? null : Text(libL10n.notExistFmt(libL10n.bioAuth), style: UIs.textGrey),\n          trailing: can\n              ? StoreSwitch(\n                  prop: Stores.setting.useBioAuth,\n                  callback: (val) async {\n                    if (val) {\n                      Stores.setting.useBioAuth.put(false);\n                      return;\n                    }\n                    // Only auth when turn off (val == false)\n                    final result = await LocalAuth.goWithResult();\n                    // If failed, turn on again\n                    if (result != AuthResult.success) {\n                      Stores.setting.useBioAuth.put(true);\n                    }\n                  },\n                )\n              : null,\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/seq/srv_detail_seq.dart",
    "content": "import 'dart:ui';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/app/server_detail_card.dart';\nimport 'package:server_box/data/res/store.dart';\n\nclass ServerDetailOrderPage extends StatefulWidget {\n  const ServerDetailOrderPage({super.key});\n\n  @override\n  State<ServerDetailOrderPage> createState() => _ServerDetailOrderPageState();\n\n  static const route = AppRouteNoArg(page: ServerDetailOrderPage.new, path: '/settings/order/server_detail');\n}\n\nclass _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {\n  final prop = Stores.setting.detailCardOrder;\n  final disabledProp = Stores.setting.detailCardDisabled;\n\n  late List<String> _order;\n  late Set<String> _enabled;\n\n  @override\n  void initState() {\n    super.initState();\n    _loadData();\n  }\n\n  void _loadData() {\n    final keys = prop.fetch();\n    final disabled = disabledProp.fetch();\n    _order = List<String>.from(keys);\n    for (final d in disabled) {\n      if (!_order.contains(d)) {\n        _order.add(d);\n      }\n    }\n    _enabled = Set<String>.from(keys.where((k) => !disabled.contains(k)));\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(title: Text(l10n.serverDetailOrder)),\n      body: SafeArea(child: _buildBody()),\n    );\n  }\n\n  Widget _proxyDecorator(Widget child, int _, Animation<double> animation) {\n    return AnimatedBuilder(\n      animation: animation,\n      builder: (BuildContext context, Widget? child) {\n        final double animValue = Curves.easeInOut.transform(animation.value);\n        final double elevation = lerpDouble(1, 6, animValue)!;\n        final double scale = lerpDouble(1, 1.02, animValue)!;\n        return Transform.scale(\n          scale: scale,\n          child: Card(elevation: elevation, child: child),\n        );\n      },\n      child: child,\n    );\n  }\n\n  Widget _buildBody() {\n    return ReorderableListView.builder(\n      key: const PageStorageKey('srv_detail_seq'),\n      padding: const EdgeInsets.all(7),\n      buildDefaultDragHandles: false,\n      itemCount: _order.length,\n      proxyDecorator: _proxyDecorator,\n      itemBuilder: (_, idx) => _buildListItem(_order[idx], idx),\n      onReorder: _handleReorder,\n    );\n  }\n\n  Widget _buildListItem(String key, int idx) {\n    final isEnabled = _enabled.contains(key);\n    return ReorderableDelayedDragStartListener(\n      key: ValueKey(key),\n      index: idx,\n      child: CardX(\n        child: ListTile(\n          contentPadding: const EdgeInsets.only(left: 23, right: 11),\n          leading: Icon(ServerDetailCards.fromName(key)?.icon),\n          title: Text(key, style: isEnabled ? null : TextStyle(color: Colors.grey)),\n          trailing: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              _buildCheckBox(key, isEnabled),\n              ReorderableDragStartListener(index: idx, child: const Icon(Icons.drag_handle)),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildCheckBox(String key, bool isEnabled) {\n    return Checkbox(\n      value: isEnabled,\n      onChanged: (_) => _toggleEnabled(key),\n    );\n  }\n\n  void _handleReorder(int oldIndex, int newIndex) {\n    var targetIndex = newIndex;\n    if (targetIndex > oldIndex) {\n      targetIndex -= 1;\n    }\n    if (targetIndex == oldIndex) {\n      return;\n    }\n\n    setState(() {\n      final item = _order.removeAt(oldIndex);\n      _order.insert(targetIndex, item);\n    });\n    _saveChanges();\n  }\n\n  void _toggleEnabled(String key) {\n    setState(() {\n      if (_enabled.contains(key)) {\n        _enabled.remove(key);\n      } else {\n        _enabled.add(key);\n      }\n    });\n    _saveChanges();\n  }\n\n  void _saveChanges() {\n    prop.put(_order);\n    final disabledList = _order.where((k) => !_enabled.contains(k)).toList();\n    disabledProp.put(disabledList);\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/seq/srv_func_seq.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/data/model/app/menu/server_func.dart';\nimport 'package:server_box/data/res/store.dart';\n\nclass ServerFuncBtnsOrderPage extends StatefulWidget {\n  const ServerFuncBtnsOrderPage({super.key});\n\n  @override\n  State<ServerFuncBtnsOrderPage> createState() => _ServerDetailOrderPageState();\n\n  static const route = AppRouteNoArg(page: ServerFuncBtnsOrderPage.new, path: '/setting/seq/srv_func');\n}\n\nclass _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {\n  final prop = Stores.setting.serverFuncBtns;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(title: Text(libL10n.sequence)),\n      body: _buildBody(),\n    );\n  }\n\n  Widget _buildBody() {\n    return ValBuilder(\n      listenable: prop.listenable(),\n      builder: (keys) {\n        final disabled = ServerFuncBtn.values.map((e) => e.index).where((e) => !keys.contains(e)).toList();\n        final allKeys = [...keys, ...disabled];\n        return ReorderableListView.builder(\n          key: const PageStorageKey('srv_func_seq'),\n          padding: const EdgeInsets.all(7),\n          itemCount: allKeys.length,\n          itemBuilder: (_, idx) => _buildListItem(allKeys[idx], idx, keys),\n          onReorder: (o, n) {\n            if (o >= keys.length || n >= keys.length) {\n              context.showSnackBar(libL10n.disabled);\n              return;\n            }\n            keys.moveByItem(o, n, property: prop);\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildListItem(int key, int idx, List<int> keys) {\n    final funcBtn = ServerFuncBtn.values[key];\n    return CardX(\n      key: ValueKey(key),\n      child: ListTile(\n        title: RichText(\n          text: TextSpan(\n            children: [\n              WidgetSpan(child: Icon(funcBtn.icon)),\n              const WidgetSpan(child: UIs.width13),\n              TextSpan(text: funcBtn.toStr, style: UIs.textGrey),\n            ],\n          ),\n        ),\n        leading: _buildCheckBox(keys, key, idx, idx < keys.length),\n      ),\n    );\n  }\n\n  Widget _buildCheckBox(List<int> keys, int key, int idx, bool value) {\n    return Checkbox(\n      value: value,\n      onChanged: (val) {\n        if (val == null) return;\n        if (val) {\n          if (idx >= keys.length) {\n            keys.add(key);\n          } else {\n            keys.insert(idx - 1, key);\n          }\n        } else {\n          keys.remove(key);\n        }\n        prop.put(keys);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/seq/srv_seq.dart",
    "content": "import 'dart:ui';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/provider/server/all.dart';\n\nclass ServerOrderPage extends ConsumerStatefulWidget {\n  const ServerOrderPage({super.key});\n\n  @override\n  ConsumerState<ServerOrderPage> createState() => _ServerOrderPageState();\n\n  static const route = AppRouteNoArg(page: ServerOrderPage.new, path: '/settings/order/server');\n}\n\nclass _ServerOrderPageState extends ConsumerState<ServerOrderPage> {\n  late List<String> _order;\n\n  @override\n  void initState() {\n    super.initState();\n    _order = List<String>.from(ref.read(serversProvider).serverOrder);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    ref.listen<ServersState>(serversProvider, (_, next) {\n      if (listEquals(_order, next.serverOrder)) {\n        return;\n      }\n      setState(() {\n        _order = List<String>.from(next.serverOrder);\n      });\n    });\n\n    return Scaffold(\n      appBar: CustomAppBar(title: Text(l10n.serverOrder)),\n      body: SafeArea(child: _buildBody()),\n    );\n  }\n\n  Widget _proxyDecorator(Widget child, int _, Animation<double> animation) {\n    return AnimatedBuilder(\n      animation: animation,\n      builder: (BuildContext context, Widget? child) {\n        final double animValue = Curves.easeInOut.transform(animation.value);\n        final double elevation = lerpDouble(1, 6, animValue)!;\n        final double scale = lerpDouble(1, 1.02, animValue)!;\n        return Transform.scale(\n          scale: scale,\n          child: Card(elevation: elevation, child: child),\n        );\n      },\n      child: child,\n    );\n  }\n\n  Widget _buildBody() {\n    final serverState = ref.watch(serversProvider);\n    final order = _order;\n\n    if (order.isEmpty) {\n      return Center(child: Text(libL10n.empty));\n    }\n    return ReorderableListView.builder(\n      footer: const SizedBox(height: 77),\n      onReorder: (oldIndex, newIndex) {\n        var targetIndex = newIndex;\n        if (targetIndex > oldIndex) {\n          targetIndex -= 1;\n        }\n        if (targetIndex == oldIndex) {\n          return;\n        }\n\n        final newOrder = List<String>.from(order);\n        final moved = newOrder.removeAt(oldIndex);\n        newOrder.insert(targetIndex, moved);\n\n        setState(() {\n          _order = newOrder;\n        });\n        ref.read(serversProvider.notifier).updateServerOrder(newOrder);\n      },\n      padding: const EdgeInsets.all(8),\n      buildDefaultDragHandles: false,\n      itemBuilder: (_, idx) {\n        final id = order[idx];\n        final spi = serverState.servers[id];\n        return _buildItem(idx, id, spi);\n      },\n      itemCount: order.length,\n      proxyDecorator: _proxyDecorator,\n    );\n  }\n\n  Widget _buildItem(int index, String id, Spi? spi) {\n    return ReorderableDelayedDragStartListener(\n      key: ValueKey('server_item_$id'),\n      index: index,\n      child: Padding(\n        padding: const EdgeInsets.only(bottom: 3),\n        child: _buildCardTile(index, spi).cardx,\n      ),\n    );\n  }\n\n  Widget _buildCardTile(int index, Spi? spi) {\n    if (spi == null) {\n      return const SizedBox();\n    }\n\n    final name = spi.name.characters.firstOrNull ?? '?';\n\n    return ListTile(\n      title: Text(spi.name, style: const TextStyle(fontWeight: FontWeight.w500)),\n      subtitle: Text(spi.oldId, style: UIs.textGrey),\n      leading: CircleAvatar(\n        child: Text(name),\n      ),\n      trailing: ReorderableDragStartListener(index: index, child: const Icon(Icons.drag_handle)),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/setting/seq/virt_key.dart",
    "content": "import 'dart:ui';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/ssh/virtual_key.dart';\nimport 'package:server_box/data/res/store.dart';\n\nclass SSHVirtKeySettingPage extends StatefulWidget {\n  const SSHVirtKeySettingPage({super.key});\n\n  @override\n  State<SSHVirtKeySettingPage> createState() => _SSHVirtKeySettingPageState();\n\n  static const route = AppRouteNoArg(\n    page: SSHVirtKeySettingPage.new,\n    path: '/settings/ssh_virt_key',\n  );\n}\n\nclass _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {\n  final prop = Stores.setting.sshVirtKeys;\n  final disabledProp = Stores.setting.sshVirtKeysDisabled;\n\n  late List<int> _order;\n  late Set<int> _enabled;\n\n  @override\n  void initState() {\n    super.initState();\n    _loadData();\n  }\n\n  void _loadData() {\n    final keys = prop.fetch();\n    final disabled = disabledProp.fetch();\n    _order = List<int>.from(keys);\n    for (final d in disabled) {\n      if (!_order.contains(d)) {\n        _order.add(d);\n      }\n    }\n    _enabled = Set<int>.from(keys.where((k) => !disabled.contains(k)));\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(title: Text(l10n.editVirtKeys)),\n      body: SafeArea(\n        child: Column(\n          children: [\n            Padding(\n              padding: const EdgeInsets.all(7),\n              child: _buildOneLineVirtKey().cardx,\n            ),\n            Expanded(child: _buildBody()),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildOneLineVirtKey() {\n    return ListTile(\n      title: Text(l10n.onlyOneLine),\n      trailing: StoreSwitch(prop: Stores.setting.horizonVirtKey),\n    );\n  }\n\n  Widget _proxyDecorator(Widget child, int _, Animation<double> animation) {\n    return AnimatedBuilder(\n      animation: animation,\n      builder: (BuildContext context, Widget? child) {\n        final double animValue = Curves.easeInOut.transform(animation.value);\n        final double elevation = lerpDouble(1, 6, animValue)!;\n        final double scale = lerpDouble(1, 1.02, animValue)!;\n        return Transform.scale(\n          scale: scale,\n          child: Card(elevation: elevation, child: child),\n        );\n      },\n      child: child,\n    );\n  }\n\n  Widget _buildBody() {\n    return ReorderableListView.builder(\n      key: const PageStorageKey('virt_key'),\n      padding: const EdgeInsets.all(7),\n      buildDefaultDragHandles: false,\n      itemCount: _order.length,\n      proxyDecorator: _proxyDecorator,\n      itemBuilder: (_, idx) => _buildListItem(_order[idx], idx),\n      onReorder: _handleReorder,\n    );\n  }\n\n  Widget _buildListItem(int key, int idx) {\n    final item = VirtKey.values[key];\n    final help = item.help;\n    final isEnabled = _enabled.contains(key);\n    return ReorderableDelayedDragStartListener(\n      key: ValueKey(key),\n      index: idx,\n      child: CardX(\n        child: ListTile(\n          title: _buildTitle(item, isEnabled),\n          subtitle: help == null ? null : Text(help, style: UIs.textGrey),\n          trailing: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              _buildCheckBox(key, isEnabled),\n              if (!isDesktop) ...[\n                const SizedBox(width: 7),\n                ReorderableDragStartListener(index: idx, child: const Icon(Icons.drag_handle)),\n              ],\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTitle(VirtKey key, bool isEnabled) {\n    final text = key.icon == null\n        ? Text(key.text)\n        : Row(\n            children: [\n              Text(key.text),\n              const SizedBox(width: 10),\n              Icon(key.icon),\n            ],\n          );\n    return IgnorePointer(\n      child: Opacity(\n        opacity: isEnabled ? 1.0 : 0.5,\n        child: text,\n      ),\n    );\n  }\n\n  Widget _buildCheckBox(int key, bool isEnabled) {\n    return Checkbox(\n      value: isEnabled,\n      onChanged: (_) => _toggleEnabled(key),\n    );\n  }\n\n  void _handleReorder(int oldIndex, int newIndex) {\n    var targetIndex = newIndex;\n    if (targetIndex > oldIndex) {\n      targetIndex -= 1;\n    }\n    if (targetIndex == oldIndex) {\n      return;\n    }\n\n    setState(() {\n      final item = _order.removeAt(oldIndex);\n      _order.insert(targetIndex, item);\n    });\n    _saveChanges();\n  }\n\n  void _toggleEnabled(int key) {\n    setState(() {\n      if (_enabled.contains(key)) {\n        _enabled.remove(key);\n      } else {\n        _enabled.add(key);\n      }\n    });\n    _saveChanges();\n  }\n\n  void _saveChanges() {\n    prop.put(_order);\n    final disabledList = _order.where((k) => !_enabled.contains(k)).toList();\n    disabledProp.put(disabledList);\n  }\n}\n"
  },
  {
    "path": "lib/view/page/snippet/edit.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_markdown_plus/flutter_markdown_plus.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/server/snippet.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/provider/snippet.dart';\n\nfinal class SnippetEditPageArgs {\n  final Snippet? snippet;\n  const SnippetEditPageArgs({this.snippet});\n}\n\nclass SnippetEditPage extends ConsumerStatefulWidget {\n  final SnippetEditPageArgs? args;\n\n  const SnippetEditPage({super.key, this.args});\n\n  @override\n  ConsumerState<SnippetEditPage> createState() => _SnippetEditPageState();\n\n  static const route = AppRoute(page: SnippetEditPage.new, path: '/snippets/edit');\n}\n\nclass _SnippetEditPageState extends ConsumerState<SnippetEditPage> with AfterLayoutMixin {\n  final _nameController = TextEditingController();\n  final _scriptController = TextEditingController();\n  final _noteController = TextEditingController();\n  final _scriptNode = FocusNode();\n  final _autoRunOn = ValueNotifier(<String>[]);\n  final _tags = <String>{}.vn;\n\n  @override\n  void dispose() {\n    super.dispose();\n    _nameController.dispose();\n    _scriptController.dispose();\n    _noteController.dispose();\n    _scriptNode.dispose();\n    _autoRunOn.dispose();\n    _tags.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(title: Text(libL10n.edit), actions: _buildAppBarActions()),\n      body: _buildBody(),\n      floatingActionButton: _buildFAB(),\n    );\n  }\n\n  List<Widget>? _buildAppBarActions() {\n    final snippet = widget.args?.snippet;\n    if (snippet == null) return null;\n    return [\n      IconButton(\n        onPressed: () {\n          context.showRoundDialog(\n            title: libL10n.attention,\n            child: Text(libL10n.askContinue('${libL10n.delete} ${libL10n.snippet}(${snippet.name})')),\n            actions: Btn.ok(\n              onTap: () {\n                ref.read(snippetProvider.notifier).del(snippet);\n                context.pop();\n                context.pop();\n              },\n              red: true,\n            ).toList,\n          );\n        },\n        tooltip: libL10n.delete,\n        icon: const Icon(Icons.delete),\n      ),\n    ];\n  }\n\n  Widget _buildFAB() {\n    return FloatingActionButton(\n      heroTag: 'snippet',\n      child: const Icon(Icons.save),\n      onPressed: () {\n        final name = _nameController.text;\n        final script = _scriptController.text;\n        if (name.isEmpty || script.isEmpty) {\n          context.showSnackBar(libL10n.empty);\n          return;\n        }\n        final note = _noteController.text;\n        final snippet = Snippet(\n          name: name,\n          script: script,\n          tags: _tags.value.isEmpty ? null : _tags.value.toList(),\n          note: note.isEmpty ? null : note,\n          autoRunOn: _autoRunOn.value.isEmpty ? null : _autoRunOn.value,\n        );\n        final oldSnippet = widget.args?.snippet;\n        final notifier = ref.read(snippetProvider.notifier);\n        if (oldSnippet != null) {\n          notifier.update(oldSnippet, snippet);\n        } else {\n          notifier.add(snippet);\n        }\n        context.pop();\n      },\n    );\n  }\n\n  Widget _buildBody() {\n    return AutoMultiList(\n      children: [\n        Input(\n          autoFocus: true,\n          controller: _nameController,\n          type: TextInputType.text,\n          onSubmitted: (_) => FocusScope.of(context).requestFocus(_scriptNode),\n          label: libL10n.name,\n          icon: Icons.info,\n          suggestion: true,\n        ),\n        Input(\n          controller: _noteController,\n          minLines: 3,\n          maxLines: 3,\n          type: TextInputType.multiline,\n          label: libL10n.note,\n          icon: Icons.note,\n          suggestion: true,\n        ),\n        Consumer(\n          builder: (_, ref, _) {\n            final tags = ref.watch(snippetProvider.select((p) => p.tags));\n            return TagTile(tags: _tags, allTags: tags).cardx;\n          },\n        ),\n        Input(\n          controller: _scriptController,\n          node: _scriptNode,\n          minLines: 3,\n          maxLines: 10,\n          type: TextInputType.multiline,\n          label: libL10n.snippet,\n          icon: Icons.code,\n          suggestion: false,\n        ),\n        _buildAutoRunOn(),\n        _buildTip(),\n      ],\n    );\n  }\n\n  Widget _buildAutoRunOn() {\n    return CardX(\n      child: ValBuilder(\n        listenable: _autoRunOn,\n        builder: (vals) {\n          final subtitle = vals.isEmpty\n              ? null\n              : vals.map((e) => ref.read(serversProvider).servers[e]?.name ?? e).join(', ');\n          return ListTile(\n            leading: const Padding(\n              padding: EdgeInsets.only(left: 5),\n              child: Icon(Icons.settings_remote, size: 19),\n            ),\n            title: Text(l10n.autoRun),\n            trailing: const Icon(Icons.keyboard_arrow_right),\n            subtitle: subtitle == null\n                ? null\n                : Text(subtitle, maxLines: 1, style: UIs.textGrey, overflow: TextOverflow.ellipsis),\n            onTap: () async {\n              // Create a filtered copy for the dialog, don't modify the original\n              final validServerIds = vals.where((e) => ref.read(serversProvider).serverOrder.contains(e)).toList();\n              final serverIds = await context.showPickDialog(\n                title: l10n.autoRun,\n                items: ref.read(serversProvider).serverOrder,\n                display: (e) => ref.read(serversProvider).servers[e]?.name ?? e,\n                initial: validServerIds,\n                clearable: true,\n              );\n              if (serverIds != null) {\n                _autoRunOn.value = serverIds;\n              }\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildTip() {\n    return CardX(\n      child: Padding(\n        padding: const EdgeInsets.all(13),\n        child: SimpleMarkdown(\n          data:\n              '''\n📌 ${l10n.supportFmtArgs}\\n\n${SnippetX.fmtArgs.keys.map((e) => '`$e`').join(', ')}\\n\n\n${SnippetX.fmtTermKeys.keys.map((e) => '`$e+?}`').join(', ')}\\n\n${libL10n.example}: \n- `\\${ctrl+c}` (Control + C)\n- `\\${ctrl+b}d` (Tmux Detach)\n''',\n          styleSheet: MarkdownStyleSheet(codeblockDecoration: const BoxDecoration(color: Colors.transparent)),\n        ),\n      ),\n    );\n  }\n\n  @override\n  void afterFirstLayout(BuildContext context) {\n    final snippet = widget.args?.snippet;\n    if (snippet != null) {\n      _nameController.text = snippet.name;\n      _scriptController.text = snippet.script;\n      if (snippet.note != null) {\n        _noteController.text = snippet.note!;\n      }\n\n      if (snippet.tags != null) {\n        _tags.value = snippet.tags!.toSet();\n      }\n\n      if (snippet.autoRunOn != null) {\n        _autoRunOn.value = snippet.autoRunOn!;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/view/page/snippet/list.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nimport 'package:server_box/data/model/server/snippet.dart';\nimport 'package:server_box/data/provider/snippet.dart';\nimport 'package:server_box/view/page/snippet/edit.dart';\n\nclass SnippetListPage extends ConsumerStatefulWidget {\n  const SnippetListPage({super.key});\n\n  @override\n  ConsumerState<SnippetListPage> createState() => _SnippetListPageState();\n\n  static const route = AppRouteNoArg(page: SnippetListPage.new, path: '/snippets');\n}\n\nclass _SnippetListPageState extends ConsumerState<SnippetListPage> with AutomaticKeepAliveClientMixin {\n  final _tag = ''.vn;\n  final _splitViewCtrl = SplitViewController();\n\n  static const _desiredItemHeight = 77.0;\n\n  @override\n  void dispose() {\n    super.dispose();\n    _tag.dispose();\n    _splitViewCtrl.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return _buildBody();\n  }\n\n  Widget _buildBody() {\n    // final isMobile = ResponsiveBreakpoints.of(context).isMobile;\n    final snippetState = ref.watch(snippetProvider);\n    final snippets = snippetState.snippets;\n    \n    return _tag.listenVal((tag) {\n      final child = _buildScaffold(snippets, tag);\n      // if (isMobile) {\n      return child;\n      // }\n\n        // return SplitView(\n        //   controller: _splitViewCtrl,\n        //   leftWeight: 1,\n        //   rightWeight: 1.3,\n        //   initialRight: Center(child: Text(libL10n.empty)),\n        //   leftBuilder: (_, __) => child,\n        // );\n    });\n  }\n\n  Widget _buildScaffold(List<Snippet> snippets, String tag) {\n    final snippetState = ref.watch(snippetProvider);\n    return Scaffold(\n      appBar: TagSwitcher(\n        tags: snippetState.tags.vn,\n        onTagChanged: (tag) => _tag.value = tag,\n        initTag: _tag.value,\n        singleLine: true,\n      ),\n      body: _buildSnippetList(snippets, tag),\n      floatingActionButton: FloatingActionButton(\n        heroTag: 'snippetAdd',\n        child: const Icon(Icons.add),\n        onPressed: () {\n          // if (ResponsiveBreakpoints.of(context).isMobile) {\n          SnippetEditPage.route.go(context);\n          // } else {\n          //   _splitViewCtrl.replace(const SnippetEditPage());\n          // }\n        },\n      ),\n    );\n  }\n\n  Widget _buildSnippetList(List<Snippet> snippets, String tag) {\n    if (snippets.isEmpty) return Center(child: Text(libL10n.empty));\n\n    final filtered = tag == TagSwitcher.kDefaultTag\n        ? snippets\n        : snippets.where((e) => e.tags?.contains(tag) ?? false).toList();\n\n    return GridView.builder(\n      padding: const EdgeInsets.symmetric(horizontal: 9),\n      gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(\n        maxCrossAxisExtent: UIs.columnWidth,\n        mainAxisExtent: _desiredItemHeight,\n      ),\n      itemCount: filtered.length,\n      itemBuilder: (context, index) {\n        final snippet = filtered[index];\n        return _buildSnippetItem(snippet);\n      },\n    );\n  }\n\n  Widget _buildSnippetItem(Snippet snippet) {\n    return ListTile(\n      contentPadding: const EdgeInsets.only(left: 23, right: 17),\n      title: Text(snippet.name, overflow: TextOverflow.ellipsis, maxLines: 1),\n      subtitle: Text(\n        snippet.note ?? snippet.script,\n        overflow: TextOverflow.ellipsis,\n        maxLines: 3,\n        style: UIs.textGrey,\n      ),\n      trailing: const Icon(Icons.keyboard_arrow_right),\n      onTap: () {\n        // final isMobile = ResponsiveBreakpoints.of(context).isMobile;\n        // if (isMobile) {\n        SnippetEditPage.route.go(context, args: SnippetEditPageArgs(snippet: snippet));\n        // } else {\n        //   _splitViewCtrl.replace(SnippetEditPage(\n        //     args: SnippetEditPageArgs(snippet: snippet),\n        //   ));\n        // }\n      },\n    ).cardx;\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n}\n"
  },
  {
    "path": "lib/view/page/snippet/result.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/server/snippet.dart';\n\nclass SnippetResultPage extends StatelessWidget {\n  final List<SnippetResult?> args;\n\n  const SnippetResultPage({super.key, required this.args});\n\n  static const route = AppRouteArg(page: SnippetResultPage.new, path: '/snippets/result');\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(title: Text(l10n.result)),\n      body: _buildBody(),\n    );\n  }\n\n  Widget _buildBody() {\n    return ListView.builder(\n      padding: const EdgeInsets.symmetric(horizontal: 17),\n      itemCount: args.length,\n      itemBuilder: (_, index) {\n        final item = args[index];\n        if (item == null) return UIs.placeholder;\n        return CardX(\n          child: ExpandTile(\n            initiallyExpanded: args.length == 1,\n            title: Text(item.dest ?? ''),\n            subtitle: Text(item.time.toString(), style: UIs.textGrey),\n            children: [\n              SingleChildScrollView(\n                padding: const EdgeInsets.symmetric(horizontal: 17),\n                scrollDirection: Axis.horizontal,\n                child: Text(item.result, textAlign: TextAlign.start),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/ssh/page/ask_ai.dart",
    "content": "part of 'page.dart';\n\nextension _AskAi on SSHPageState {\n  List<ContextMenuButtonItem> _buildTerminalToolbar(\n    BuildContext context,\n    CustomTextEditState state,\n    List<ContextMenuButtonItem> defaultItems,\n  ) {\n    final rawSelection = _termKey.currentState?.renderTerminal.selectedText;\n    final selection = rawSelection?.trim();\n    if (selection == null || selection.isEmpty) {\n      return defaultItems;\n    }\n\n    final items = List<ContextMenuButtonItem>.from(defaultItems);\n    items.add(\n      ContextMenuButtonItem(\n        label: context.l10n.askAi,\n        onPressed: () {\n          state.hideToolbar();\n          _showAskAiSheet(selection);\n        },\n      ),\n    );\n    return items;\n  }\n\n  Future<void> _showAskAiSheet(String selection) async {\n    if (!mounted) return;\n    final localeHint = Localizations.maybeLocaleOf(context)?.toLanguageTag();\n    await showModalBottomSheet<void>(\n      context: context,\n      isScrollControlled: true,\n      useSafeArea: true,\n      builder: (ctx) {\n        return _AskAiSheet(selection: selection, localeHint: localeHint, onCommandApply: _applyAiCommand);\n      },\n    );\n  }\n\n  void _applyAiCommand(String command) {\n    if (command.isEmpty) {\n      return;\n    }\n    _terminal.textInput(command);\n    (widget.args.focusNode?.requestFocus ?? _termKey.currentState?.requestKeyboard)?.call();\n  }\n}\n\nclass _AskAiSheet extends ConsumerStatefulWidget {\n  const _AskAiSheet({required this.selection, required this.localeHint, required this.onCommandApply});\n\n  final String selection;\n  final String? localeHint;\n  final ValueChanged<String> onCommandApply;\n\n  @override\n  ConsumerState<_AskAiSheet> createState() => _AskAiSheetState();\n}\n\nenum _ChatEntryType { user, assistant, command }\n\nclass _ChatEntry {\n  const _ChatEntry._({required this.type, this.content, this.command});\n\n  const _ChatEntry.user(String content) : this._(type: _ChatEntryType.user, content: content);\n\n  const _ChatEntry.assistant(String content) : this._(type: _ChatEntryType.assistant, content: content);\n\n  const _ChatEntry.command(AskAiCommand command) : this._(type: _ChatEntryType.command, command: command);\n\n  final _ChatEntryType type;\n  final String? content;\n  final AskAiCommand? command;\n}\n\nclass _AskAiSheetState extends ConsumerState<_AskAiSheet> {\n  StreamSubscription<AskAiEvent>? _subscription;\n  final _chatEntries = <_ChatEntry>[];\n  final _history = <AskAiMessage>[];\n  final _scrollController = ScrollController();\n  final _inputController = TextEditingController();\n  final _seenCommands = <String>{};\n  String? _streamingContent;\n  String? _error;\n  bool _isStreaming = false;\n  bool _isMinimized = false;\n\n  @override\n  void initState() {\n    super.initState();\n    _inputController.addListener(_handleInputChanged);\n    _startStream();\n  }\n\n  @override\n  void dispose() {\n    _subscription?.cancel();\n    _scrollController.dispose();\n    _inputController\n      ..removeListener(_handleInputChanged)\n      ..dispose();\n    super.dispose();\n  }\n\n  void _handleInputChanged() {\n    if (!mounted) return;\n    setState(() {});\n  }\n\n  void _startStream() {\n    _subscription?.cancel();\n    setState(() {\n      _isStreaming = true;\n      _error = null;\n      _streamingContent = '';\n    });\n\n    final messages = List<AskAiMessage>.from(_history);\n\n    _subscription = ref\n        .read(askAiRepositoryProvider)\n        .ask(selection: widget.selection, localeHint: widget.localeHint, conversation: messages)\n        .listen(\n          _handleEvent,\n          onError: (error, stack) {\n            if (!mounted) return;\n            setState(() {\n              _error = _describeError(error);\n              _isStreaming = false;\n              _streamingContent = null;\n            });\n          },\n          onDone: () {\n            if (!mounted) return;\n            setState(() {\n              _isStreaming = false;\n            });\n          },\n        );\n  }\n\n  void _handleEvent(AskAiEvent event) {\n    if (!mounted) return;\n    var shouldScroll = false;\n    setState(() {\n      if (event is AskAiContentDelta) {\n        _streamingContent = (_streamingContent ?? '') + event.delta;\n        shouldScroll = true;\n      } else if (event is AskAiToolSuggestion) {\n        final inserted = _seenCommands.add(event.command.command);\n        if (inserted) {\n          _chatEntries.add(_ChatEntry.command(event.command));\n          shouldScroll = true;\n        }\n      } else if (event is AskAiCompleted) {\n        final fullText = event.fullText.isNotEmpty ? event.fullText : (_streamingContent ?? '');\n        if (fullText.trim().isNotEmpty) {\n          final message = AskAiMessage(role: AskAiMessageRole.assistant, content: fullText);\n          _history.add(message);\n          _chatEntries.add(_ChatEntry.assistant(fullText));\n        }\n        for (final command in event.commands) {\n          final inserted = _seenCommands.add(command.command);\n          if (inserted) {\n            _chatEntries.add(_ChatEntry.command(command));\n          }\n        }\n        _streamingContent = null;\n        _isStreaming = false;\n        shouldScroll = true;\n      } else if (event is AskAiStreamError) {\n        _error = _describeError(event.error);\n        _streamingContent = null;\n        _isStreaming = false;\n      }\n    });\n\n    if (shouldScroll) {\n      _scheduleAutoScroll();\n    }\n  }\n\n  void _scheduleAutoScroll() {\n    if (!_scrollController.hasClients) return;\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (!_scrollController.hasClients) return;\n      _scrollController.animateTo(\n        _scrollController.position.maxScrollExtent,\n        duration: const Duration(milliseconds: 180),\n        curve: Curves.easeOutCubic,\n      );\n    });\n  }\n\n  String _describeError(Object error) {\n    final l10n = context.l10n;\n    if (error is AskAiConfigException) {\n      if (error.missingFields.isEmpty) {\n        if (error.hasInvalidBaseUrl) {\n          return 'Invalid Ask AI base URL: ${error.invalidBaseUrl}';\n        }\n        return error.toString();\n      }\n      final locale = Localizations.maybeLocaleOf(context);\n      final separator = switch (locale?.languageCode) {\n        'zh' => '、',\n        'ja' => '、',\n        _ => ', ',\n      };\n      final formattedFields = error.missingFields\n          .map(\n            (field) => switch (field) {\n              AskAiConfigField.baseUrl => l10n.askAiBaseUrl,\n              AskAiConfigField.apiKey => l10n.askAiApiKey,\n              AskAiConfigField.model => libL10n.askAiModel,\n            },\n          )\n          .join(separator);\n      final message = l10n.askAiConfigMissing(formattedFields);\n      if (error.hasInvalidBaseUrl) {\n        return '$message (invalid URL: ${error.invalidBaseUrl})';\n      }\n      return message;\n    }\n    if (error is AskAiNetworkException) {\n      return error.message;\n    }\n    return error.toString();\n  }\n\n  Future<void> _handleApplyCommand(BuildContext context, AskAiCommand command) async {\n    final confirmed = await context.showRoundDialog<bool>(\n      title: context.l10n.askAiConfirmExecute,\n      child: SelectableText(command.command, style: const TextStyle(fontFamily: 'monospace')),\n      actions: [\n        TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),\n        TextButton(onPressed: () => context.pop(true), child: Text(libL10n.ok)),\n      ],\n    );\n    if (confirmed == true) {\n      widget.onCommandApply(command.command);\n      if (!mounted) return;\n      context.showSnackBar(context.l10n.askAiCommandInserted);\n    }\n  }\n\n  Future<void> _copyCommand(BuildContext context, AskAiCommand command) async {\n    await Clipboard.setData(ClipboardData(text: command.command));\n    if (!mounted) return;\n    context.showSnackBar(libL10n.success);\n  }\n\n  Future<void> _copyText(BuildContext context, String text) async {\n    if (text.trim().isEmpty) return;\n    await Clipboard.setData(ClipboardData(text: text));\n    if (!mounted) return;\n    context.showSnackBar(libL10n.success);\n  }\n\n  void _sendMessage() {\n    if (_isStreaming) return;\n    final text = _inputController.text.trim();\n    if (text.isEmpty) return;\n    setState(() {\n      final message = AskAiMessage(role: AskAiMessageRole.user, content: text);\n      _history.add(message);\n      _chatEntries.add(_ChatEntry.user(text));\n      _inputController.clear();\n    });\n    _startStream();\n    _scheduleAutoScroll();\n  }\n\n  List<Widget> _buildConversationWidgets(BuildContext context, ThemeData theme) {\n    final widgets = <Widget>[];\n    for (final entry in _chatEntries) {\n      widgets.add(_buildChatItem(context, theme, entry));\n      widgets.add(const SizedBox(height: 12));\n    }\n\n    if (_streamingContent != null) {\n      widgets.add(_buildAssistantBubble(theme, content: _streamingContent!, streaming: true));\n      widgets.add(const SizedBox(height: 12));\n    } else if (_chatEntries.isEmpty && _error == null) {\n      widgets.add(_buildAssistantBubble(theme, content: '', streaming: true));\n      widgets.add(const SizedBox(height: 12));\n    }\n\n    if (widgets.isNotEmpty) {\n      widgets.removeLast();\n    }\n    return widgets;\n  }\n\n  Widget _buildChatItem(BuildContext context, ThemeData theme, _ChatEntry entry) {\n    switch (entry.type) {\n      case _ChatEntryType.user:\n        return Align(\n          alignment: Alignment.centerRight,\n          child: CardX(\n            child: Padding(padding: const EdgeInsets.all(12), child: SelectableText(entry.content ?? '')),\n          ),\n        );\n      case _ChatEntryType.assistant:\n        return _buildAssistantBubble(theme, content: entry.content ?? '');\n      case _ChatEntryType.command:\n        final command = entry.command!;\n        return _buildCommandBubble(context, theme, command);\n    }\n  }\n\n  Widget _buildAssistantBubble(ThemeData theme, {required String content, bool streaming = false}) {\n    final trimmed = content.trim();\n    final l10n = context.l10n;\n    final child = trimmed.isEmpty\n        ? Text(\n            streaming ? l10n.askAiAwaitingResponse : l10n.askAiNoResponse,\n            style: theme.textTheme.bodySmall,\n          )\n        : Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              SimpleMarkdown(data: content),\n              const SizedBox(height: 8),\n              Row(\n                mainAxisAlignment: MainAxisAlignment.end,\n                children: [\n                  TextButton.icon(\n                    onPressed: () => _copyText(context, content),\n                    icon: const Icon(Icons.copy, size: 18),\n                    label: Text(libL10n.copy),\n                  ),\n                ],\n              ),\n            ],\n          );\n    return Align(\n      alignment: Alignment.centerLeft,\n      child: CardX(\n        child: Padding(padding: const EdgeInsets.all(12), child: child),\n      ),\n    );\n  }\n\n  Widget _buildCommandBubble(BuildContext context, ThemeData theme, AskAiCommand command) {\n    final l10n = context.l10n;\n    return Align(\n      alignment: Alignment.centerLeft,\n      child: CardX(\n        child: Padding(\n          padding: const EdgeInsets.all(12),\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Text(l10n.askAiRecommendedCommand, style: theme.textTheme.labelMedium),\n              const SizedBox(height: 8),\n              SelectableText(command.command, style: const TextStyle(fontFamily: 'monospace')),\n              if (command.description.isNotEmpty) ...[\n                const SizedBox(height: 6),\n                Text(command.description, style: theme.textTheme.bodySmall),\n              ],\n              const SizedBox(height: 12),\n              Row(\n                mainAxisAlignment: MainAxisAlignment.end,\n                children: [\n                  TextButton.icon(\n                    onPressed: () => _copyCommand(context, command),\n                    icon: const Icon(Icons.copy, size: 18),\n                    label: Text(libL10n.copy),\n                  ),\n                  const SizedBox(width: 8),\n                  FilledButton.icon(\n                    onPressed: () => _handleApplyCommand(context, command),\n                    icon: const Icon(Icons.terminal, size: 18),\n                    label: Text(l10n.askAiInsertTerminal),\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    final bottomPadding = MediaQuery.viewInsetsOf(context).bottom;\n    final heightFactor = _isMinimized ? 0.18 : 0.85;\n\n    return TweenAnimationBuilder<double>(\n      tween: Tween<double>(end: heightFactor),\n      duration: const Duration(milliseconds: 200),\n      curve: Curves.easeOutCubic,\n      builder: (context, animatedHeightFactor, child) {\n        return ClipRect(\n          child: FractionallySizedBox(\n            heightFactor: animatedHeightFactor,\n            child: child,\n          ),\n        );\n      },\n      child: SafeArea(\n            child: Column(\n              children: [\n            Padding(\n              padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),\n              child: Row(\n                children: [\n                  Text(context.l10n.askAi, style: theme.textTheme.titleLarge),\n                  const SizedBox(width: 8),\n                  if (_isStreaming)\n                    const SizedBox(height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2)),\n                  const Spacer(),\n                  IconButton(\n                    icon: Icon(_isMinimized ? Icons.unfold_more : Icons.unfold_less),\n                    tooltip: libL10n.fold,\n                    onPressed: () {\n                      FocusManager.instance.primaryFocus?.unfocus();\n                      setState(() {\n                        _isMinimized = !_isMinimized;\n                      });\n                    },\n                  ),\n                  IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()),\n                ],\n              ),\n            ),\n            if (!_isMinimized) ...[\n              Expanded(\n                child: Scrollbar(\n                  controller: _scrollController,\n                  child: ListView(\n                    controller: _scrollController,\n                    padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),\n                    children: [\n                      Text(context.l10n.askAiSelectedContent, style: theme.textTheme.titleMedium),\n                      const SizedBox(height: 6),\n                      CardX(\n                        child: Padding(\n                          padding: const EdgeInsets.all(12),\n                          child: SelectableText(\n                            widget.selection,\n                            style: const TextStyle(fontFamily: 'monospace'),\n                          ),\n                        ),\n                      ),\n                      const SizedBox(height: 16),\n                      Text(context.l10n.askAiConversation, style: theme.textTheme.titleMedium),\n                      const SizedBox(height: 6),\n                      ..._buildConversationWidgets(context, theme),\n                      if (_error != null) ...[\n                        const SizedBox(height: 16),\n                        CardX(\n                          child: Padding(\n                            padding: const EdgeInsets.all(12),\n                            child: Text(_error!, style: TextStyle(color: theme.colorScheme.error)),\n                          ),\n                        ),\n                      ],\n                      if (_isStreaming) ...[const SizedBox(height: 16), const LinearProgressIndicator()],\n                      const SizedBox(height: 16),\n                    ],\n                  ),\n                ),\n              ),\n              Padding(\n                padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),\n                child: Text(\n                  context.l10n.askAiDisclaimer,\n                  style: theme.textTheme.bodySmall?.copyWith(\n                        color: theme.colorScheme.error,\n                        fontWeight: FontWeight.bold,\n                      ),\n                  textAlign: TextAlign.center,\n                ),\n              ),\n              Padding(\n                padding: EdgeInsets.fromLTRB(16, 8, 16, 16 + bottomPadding),\n                child: Row(\n                  children: [\n                    Expanded(\n                      child: Input(\n                        controller: _inputController,\n                        minLines: 1,\n                        maxLines: 4,\n                        hint: context.l10n.askAiFollowUpHint,\n                        action: TextInputAction.send,\n                        onSubmitted: (_) => _sendMessage(),\n                      ),\n                    ),\n                    const SizedBox(width: 12),\n                    Btn.icon(\n                      onTap: _isStreaming || _inputController.text.trim().isEmpty ? null : _sendMessage,\n                      icon: const Icon(Icons.send, size: 18),\n                    ),\n                  ],\n                ).cardx,\n              ),\n            ] else\n              const SizedBox(height: 8),\n              ],\n            ),\n          ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/ssh/page/init.dart",
    "content": "part of 'page.dart';\n\nextension _Init on SSHPageState {\n  void _initStoredCfg() {\n    final fontFamilly = Stores.setting.fontPath.fetch().getFileName();\n    final textSize = Stores.setting.termFontSize.fetch();\n    final textStyle = TextStyle(fontFamily: fontFamilly, fontSize: textSize);\n\n    _terminalStyle = TerminalStyle.fromTextStyle(textStyle);\n  }\n\n  Future<void> _showHelp() async {\n    if (Stores.setting.sshTermHelpShown.fetch()) return;\n\n    return await context.showRoundDialog(\n      title: libL10n.doc,\n      child: Text(l10n.sshTermHelp),\n      actions: [\n        TextButton(\n          onPressed: () {\n            Stores.setting.sshTermHelpShown.put(true);\n            context.pop();\n          },\n          child: Text(l10n.noPromptAgain),\n        ),\n      ],\n    );\n  }\n\n  Future<void> _initTerminal() async {\n    _writeLn(l10n.waitConnection);\n    _client ??= await genClient(\n      widget.args.spi,\n      onStatus: (p0) {\n        _writeLn(p0.toString());\n      },\n      onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(widget.args.spi, ctx: context),\n    );\n\n    _writeLn('${libL10n.execute}: Shell');\n    final session = await _client?.shell(\n      pty: SSHPtyConfig(width: _terminal.viewWidth, height: _terminal.viewHeight),\n      environment: widget.args.spi.envs,\n    );\n\n    if (session == null) {\n      _writeLn(libL10n.fail);\n      return;\n    }\n\n    _terminal.buffer.clear();\n    _terminal.buffer.setCursor(0, 0);\n\n    _terminal.onOutput = (data) {\n      session.write(utf8.encode(data));\n    };\n    _terminal.onResize = (width, height, pixelWidth, pixelHeight) {\n      session.resizeTerminal(width, height);\n    };\n\n    _listen(session.stdout);\n    _listen(session.stderr);\n\n    // Hold the session for external control (disconnect)\n    _session = session;\n    // Mark status connected for notifications / live activities\n    TermSessionManager.updateStatus(_sessionId, TermSessionStatus.connected);\n\n    final snippets = ref.read(snippetProvider.select((p) => p.snippets));\n    for (final snippet in snippets) {\n      if (snippet.autoRunOn?.contains(widget.args.spi.id) == true) {\n        snippet.runInTerm(_terminal, widget.args.spi);\n      }\n    }\n\n    final initCmd = widget.args.initCmd;\n    if (initCmd != null) {\n      _terminal.textInput(initCmd);\n      _terminal.keyInput(TerminalKey.enter);\n    }\n\n    final initSnippet = widget.args.initSnippet;\n    if (initSnippet != null) {\n      initSnippet.runInTerm(_terminal, widget.args.spi);\n    }\n\n    widget.args.focusNode?.requestFocus();\n\n    await session.done;\n    if (mounted && widget.args.notFromTab) {\n      context.pop();\n    }\n    widget.args.onSessionEnd?.call();\n    TermSessionManager.remove(_sessionId);\n  }\n\n  void _listen(Stream<Uint8List>? stream) {\n    if (stream == null) {\n      return;\n    }\n\n    stream\n        .cast<List<int>>()\n        .transform(const Utf8Decoder())\n        .listen(\n          _terminal.write,\n          onError: (Object error, StackTrace stack) {\n            // _terminal.write('Stream error: $error\\n');\n            Loggers.root.warning('Error in SSH stream', error, stack);\n          },\n          cancelOnError: false,\n        );\n  }\n\n  void _setupDiscontinuityTimer() {\n    _discontinuityTimer?.cancel();\n    if (!mounted) return;\n\n    _missedKeepAliveCount = 0;\n    _discontinuityTimer = Timer.periodic(\n      SSHPageState._connectionCheckInterval,\n      (_) => _checkConnectionHealth(),\n    );\n  }\n\n  Future<void> _checkConnectionHealth() async {\n    if (!mounted || _client == null || _isCheckingConnection) return;\n    _isCheckingConnection = true;\n\n    try {\n      await _client!.ping().timeout(SSHPageState._connectionCheckTimeout);\n      _missedKeepAliveCount = 0;\n      if (_reportedDisconnected) {\n        _reportedDisconnected = false;\n        TermSessionManager.updateStatus(_sessionId, TermSessionStatus.connected);\n      }\n    } on TimeoutException catch (error) {\n      _handleConnectionCheckFailure(error);\n    } on Object catch (error, stackTrace) {\n      _handleConnectionCheckFailure(error, stackTrace);\n    } finally {\n      _isCheckingConnection = false;\n    }\n  }\n\n  void _handleConnectionCheckFailure(Object error, [StackTrace? stackTrace]) {\n    Loggers.root.warning('SSH keep-alive failed', error, stackTrace);\n    _missedKeepAliveCount += 1;\n\n    if (_missedKeepAliveCount < SSHPageState._maxKeepAliveFailures) {\n      return;\n    }\n\n    _missedKeepAliveCount = 0;\n    _onConnectionLossSuspected();\n  }\n\n  void _onConnectionLossSuspected() {\n    if (!mounted || _disconnectDialogOpen) return;\n\n    _disconnectDialogOpen = true;\n    _reportedDisconnected = true;\n    _discontinuityTimer?.cancel();\n    _writeLn('\\n\\nConnection lost\\r\\n');\n    TermSessionManager.updateStatus(_sessionId, TermSessionStatus.disconnected);\n    unawaited(_showDisconnectDialog());\n  }\n\n  Future<void> _showDisconnectDialog() async {\n    final shouldLeave = await context.showRoundDialog<bool>(\n      title: libL10n.attention,\n      child: Text('${libL10n.disconnected}\\n${l10n.goBackQ}'),\n      barrierDismiss: false,\n      actions: [\n        TextButton(onPressed: () => context.pop(false), child: Text(libL10n.cancel)),\n        TextButton(onPressed: () => context.pop(true), child: Text(libL10n.ok)),\n      ],\n    );\n\n    if (!mounted) return;\n\n    _disconnectDialogOpen = false;\n\n    if (shouldLeave == true) {\n      contextSafe?.pop(); // Pop the SSHPage\n      return;\n    }\n\n    _reportedDisconnected = false;\n    TermSessionManager.updateStatus(_sessionId, TermSessionStatus.connected);\n    _setupDiscontinuityTimer();\n  }\n\n  void _writeLn(String p0) {\n    _terminal.write('$p0\\r\\n');\n  }\n}\n\nextension on SSHPageState {\n  void _disconnectFromNotification() {\n    // Mark as disconnected in session manager for immediate UI/notification feedback\n    TermSessionManager.updateStatus(_sessionId, TermSessionStatus.disconnected);\n\n    // Try to close the running SSH session, if any\n    try {\n      _session?.close();\n    } catch (e, stackTrace) {\n      Loggers.app.warning('Error closing SSH session: $e\\n$stackTrace');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/view/page/ssh/page/keyboard.dart",
    "content": "part of 'page.dart';\n\nextension _Keyboard on SSHPageState {\n  void _handleEscKeyOrBackButton() {\n    _terminal.keyInput(TerminalKey.escape);\n    HapticFeedback.lightImpact();\n  }\n\n  bool _handleKeyEvent(KeyEvent event) {\n    if (event is KeyDownEvent) {\n      if (event.logicalKey == LogicalKeyboardKey.escape) {\n        // Prevent default behavior and send to terminal\n        _handleEscKeyOrBackButton();\n        return true; // Mark as handled so it doesn't propagate\n      }\n      if (event.logicalKey == LogicalKeyboardKey.shiftLeft ||\n          event.logicalKey == LogicalKeyboardKey.shiftRight) {\n        // Handle shift key press\n        _terminal.keyInput(TerminalKey.shift);\n        HapticFeedback.lightImpact();\n        return true;\n      }\n    }\n    return false; // Let other handlers process this event\n  }\n}\n"
  },
  {
    "path": "lib/view/page/ssh/page/page.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:ui';\n\nimport 'package:dartssh2/dartssh2.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/chan.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/utils/server.dart';\nimport 'package:server_box/core/utils/ssh_auth.dart';\nimport 'package:server_box/data/model/ai/ask_ai_models.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/snippet.dart';\nimport 'package:server_box/data/model/ssh/virtual_key.dart';\nimport 'package:server_box/data/provider/ai/ask_ai.dart';\nimport 'package:server_box/data/provider/server/single.dart';\nimport 'package:server_box/data/provider/snippet.dart';\nimport 'package:server_box/data/provider/virtual_keyboard.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/data/res/terminal.dart';\nimport 'package:server_box/data/ssh/session_manager.dart';\nimport 'package:server_box/view/page/storage/sftp.dart';\nimport 'package:wakelock_plus/wakelock_plus.dart';\nimport 'package:xterm/core.dart';\nimport 'package:xterm/ui.dart' hide TerminalThemes;\n\npart 'ask_ai.dart';\npart 'init.dart';\npart 'keyboard.dart';\npart 'virt_key.dart';\n\nfinal class SshPageArgs {\n  final Spi spi;\n  final String? initCmd;\n  final Snippet? initSnippet;\n  final bool notFromTab;\n  final Function()? onSessionEnd;\n  final GlobalKey<TerminalViewState>? terminalKey;\n  final FocusNode? focusNode;\n\n  const SshPageArgs({\n    required this.spi,\n    this.initCmd,\n    this.initSnippet,\n    this.notFromTab = true,\n    this.onSessionEnd,\n    this.terminalKey,\n    this.focusNode,\n  });\n}\n\nclass SSHPage extends ConsumerStatefulWidget {\n  final SshPageArgs args;\n\n  const SSHPage({super.key, required this.args});\n\n  @override\n  ConsumerState<SSHPage> createState() => SSHPageState();\n\n  static const route = AppRouteArg<void, SshPageArgs>(page: SSHPage.new, path: '/ssh/page');\n}\n\nconst _horizonPadding = 7.0;\n\nclass SSHPageState extends ConsumerState<SSHPage>\n    with AutomaticKeepAliveClientMixin, AfterLayoutMixin, TickerProviderStateMixin {\n  late final _terminal = Terminal();\n  late final TerminalController _terminalController = TerminalController(vsync: this);\n  final List<List<VirtKey>> _virtKeysList = [];\n  late final _termKey = widget.args.terminalKey ?? GlobalKey<TerminalViewState>();\n\n  late MediaQueryData _media;\n  late TerminalStyle _terminalStyle;\n  late TerminalTheme _terminalTheme;\n  double _virtKeysHeight = 0;\n  late final _horizonVirtKeys = Stores.setting.horizonVirtKey.fetch();\n\n  bool _isDark = false;\n  Timer? _virtKeyLongPressTimer;\n  SSHClient? _client;\n  SSHSession? _session;\n  Timer? _discontinuityTimer;\n  static const _connectionCheckInterval = Duration(seconds: 60);\n  static const _connectionCheckTimeout = Duration(seconds: 30);\n  static const _maxKeepAliveFailures = 3;\n  int _missedKeepAliveCount = 0;\n  bool _isCheckingConnection = false;\n  bool _disconnectDialogOpen = false;\n  bool _reportedDisconnected = false;\n\n  /// Used for (de)activate the wake lock and forground service\n  static var _sshConnCount = 0;\n  late final String _sessionId = ShortId.generate();\n  late final int _sessionStartMs = DateTime.now().millisecondsSinceEpoch;\n\n  @override\n  void dispose() {\n    _virtKeyLongPressTimer?.cancel();\n    _terminalController.dispose();\n    _discontinuityTimer?.cancel();\n\n    HardwareKeyboard.instance.removeHandler(_handleKeyEvent);\n\n    if (--_sshConnCount <= 0) {\n      WakelockPlus.disable();\n      if (isAndroid) {\n        MethodChans.stopService();\n      }\n    }\n\n    // Remove session entry\n    TermSessionManager.remove(_sessionId);\n\n    super.dispose();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _initStoredCfg();\n    _initVirtKeys();\n    _setupDiscontinuityTimer();\n    \n    // Initialize client from provider\n    final serverState = ref.read(serverProvider(widget.args.spi.id));\n    _client = serverState.client;\n\n    if (++_sshConnCount == 1) {\n      WakelockPlus.enable();\n      if (isAndroid) {\n        MethodChans.startService();\n      }\n    }\n\n    // Add session entry (for Android notifications & iOS Live Activities)\n    TermSessionManager.add(\n      id: _sessionId,\n      spi: widget.args.spi,\n      startTimeMs: _sessionStartMs,\n      disconnect: _disconnectFromNotification,\n      status: TermSessionStatus.connecting,\n    );\n    TermSessionManager.setActive(_sessionId, hasTerminal: true);\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    _isDark = switch (Stores.setting.termTheme.fetch()) {\n      1 => false,\n      2 => true,\n      _ => context.isDark,\n    };\n    _media = context.mediaQuery;\n\n    _terminalTheme = _isDark ? TerminalThemes.dark : TerminalThemes.light;\n    _terminalTheme = _terminalTheme.copyWith(selectionCursor: UIs.primaryColor);\n\n    // Because the virtual keyboard only displayed on mobile devices\n    if (isMobile) {\n      if (_horizonVirtKeys) {\n        _virtKeysHeight = 37;\n      } else {\n        _virtKeysHeight = 37.0 * _virtKeysList.length;\n      }\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    final bgImage = Stores.setting.sshBgImage.fetch();\n    final hasBg = bgImage.isNotEmpty;\n    Widget child = PopScope(\n      canPop: false,\n      onPopInvokedWithResult: (didPop, _) {\n        if (didPop) return;\n        _handleEscKeyOrBackButton();\n      },\n      child: Scaffold(\n        appBar: widget.args.notFromTab\n            ? CustomAppBar(\n                leading: BackButton(onPressed: context.pop),\n                title: Text(widget.args.spi.name),\n                centerTitle: false,\n              )\n            : null,\n        backgroundColor: hasBg ? Colors.transparent : _terminalTheme.background,\n        body: _buildBody(),\n        bottomNavigationBar: isDesktop ? null : _buildBottom(),\n      ),\n    );\n\n    if (isIOS) {\n      child = AnnotatedRegion(\n        value: _isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark,\n        child: child,\n      );\n    }\n    return child;\n  }\n\n  Widget _buildBody() {\n    final letterCache = Stores.setting.letterCache.fetch();\n    final bgImage = Stores.setting.sshBgImage.fetch();\n    final opacity = Stores.setting.sshBgOpacity.fetch();\n    final blur = Stores.setting.sshBlurRadius.fetch();\n    final file = File(bgImage);\n    final hasBg = bgImage.isNotEmpty && file.existsSync();\n    final theme = hasBg ? _terminalTheme.copyWith(background: Colors.transparent) : _terminalTheme;\n    final children = <Widget>[];\n    if (hasBg) {\n      children.add(\n        Positioned.fill(\n          child: Image.file(file, fit: BoxFit.cover, errorBuilder: (_, _, _) => const SizedBox()),\n        ),\n      );\n      if (blur > 0) {\n        children.add(\n          Positioned.fill(\n            child: BackdropFilter(\n              filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),\n              child: const SizedBox(),\n            ),\n          ),\n        );\n      }\n      children.add(\n        Positioned.fill(\n          child: ColoredBox(color: _terminalTheme.background.withValues(alpha: opacity)),\n        ),\n      );\n    }\n    children.add(\n      Padding(\n        padding: EdgeInsets.only(left: _horizonPadding, right: _horizonPadding),\n        child: TerminalView(\n          _terminal,\n          key: _termKey,\n          controller: _terminalController,\n          keyboardType: TextInputType.text,\n          enableSuggestions: letterCache,\n          textStyle: _terminalStyle,\n          backgroundOpacity: 0,\n          theme: theme,\n          deleteDetection: isMobile,\n          autofocus: false,\n          keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,\n          showToolbar: true,\n          viewOffset: Offset(2 * _horizonPadding, CustomAppBar.sysStatusBarHeight),\n          hideScrollBar: false,\n          focusNode: widget.args.focusNode,\n          toolbarBuilder: _buildTerminalToolbar,\n          onCopied: _onTerminalCopied,\n          onSelectAll: _onTerminalSelectAll,\n          onPaste: _onTerminalPaste,\n        ),\n      ),\n    );\n\n    return SizedBox(\n      height: _media.size.height - _virtKeysHeight - _media.padding.bottom - _media.padding.top,\n      child: Stack(children: children),\n    );\n  }\n\n  Widget _buildBottom() {\n    return SafeArea(\n      top: false,\n      child: AnimatedPadding(\n        padding: _media.viewInsets,\n        duration: const Duration(milliseconds: 23),\n        curve: Curves.fastOutSlowIn,\n        child: Container(\n          color: _terminalTheme.background,\n          height: _virtKeysHeight,\n          child: Consumer(\n            builder: (context, ref, child) {\n              final virtKeyState = ref.watch(virtKeyboardProvider);\n              final virtKeyNotifier = ref.read(virtKeyboardProvider.notifier);\n              \n              // Set the terminal input handler\n              _terminal.inputHandler = virtKeyNotifier;\n              \n              return _buildVirtualKey(virtKeyState, virtKeyNotifier);\n            },\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildVirtualKey(VirtKeyState virtKeyState, VirtKeyboard virtKeyNotifier) {\n    final count = _horizonVirtKeys ? _virtKeysList.length : _virtKeysList.firstOrNull?.length ?? 0;\n    if (count == 0) return UIs.placeholder;\n    return LayoutBuilder(\n      builder: (_, cons) {\n        final virtKeyWidth = cons.maxWidth / count;\n        if (_horizonVirtKeys) {\n          return SingleChildScrollView(\n            scrollDirection: Axis.horizontal,\n            child: Row(\n              children: _virtKeysList\n                  .expand((e) => e)\n                  .map((e) => _buildVirtKeyItem(e, virtKeyWidth, virtKeyState, virtKeyNotifier))\n                  .toList(),\n            ),\n          );\n        }\n        final rows = _virtKeysList\n            .map((e) => Row(children: e.map((e) => _buildVirtKeyItem(e, virtKeyWidth, virtKeyState, virtKeyNotifier)).toList()))\n            .toList();\n        return Column(mainAxisSize: MainAxisSize.min, children: rows);\n      },\n    );\n  }\n\n  Widget _buildVirtKeyItem(VirtKey item, double virtKeyWidth, VirtKeyState virtKeyState, VirtKeyboard virtKeyNotifier) {\n    var selected = false;\n    switch (item.key) {\n      case TerminalKey.control:\n        selected = virtKeyState.ctrl;\n        break;\n      case TerminalKey.alt:\n        selected = virtKeyState.alt;\n        break;\n      case TerminalKey.shift:\n        selected = virtKeyState.shift;\n        break;\n      default:\n        break;\n    }\n\n    final child = item.icon != null\n        ? Icon(item.icon, size: 17, color: _isDark ? Colors.white : Colors.black)\n        : Text(\n            item.text,\n            style: TextStyle(\n              color: selected ? UIs.primaryColor : (_isDark ? Colors.white : Colors.black),\n              fontSize: 15,\n            ),\n          );\n\n    return InkWell(\n      onTap: () => _doVirtualKey(item, virtKeyNotifier),\n      onTapDown: (details) {\n        if (item.canLongPress) {\n          _virtKeyLongPressTimer = Timer.periodic(\n            const Duration(milliseconds: 137),\n            (_) => _doVirtualKey(item, virtKeyNotifier),\n          );\n        }\n      },\n      onTapCancel: () => _virtKeyLongPressTimer?.cancel(),\n      onTapUp: (_) => _virtKeyLongPressTimer?.cancel(),\n      child: SizedBox(\n        width: virtKeyWidth,\n        height: _horizonVirtKeys ? _virtKeysHeight : _virtKeysHeight / _virtKeysList.length,\n        child: Center(child: child),\n      ),\n    );\n  }\n\n  void _onTerminalCopied() {\n    if (!mounted) return;\n    context.showSnackBar(libL10n.success);\n    _terminalController.clearSelection();\n  }\n\n  void _onTerminalSelectAll() {\n    if (!mounted) return;\n    _termKey.currentState?.renderTerminal.selectAll();\n  }\n\n  Future<void> _onTerminalPaste() async {\n    final value = await Clipboard.getData(Clipboard.kTextPlain);\n    if (!mounted) return;\n    final text = value?.text;\n    if (text == null) return;\n    _terminal.textInput(text);\n    _terminalController.clearSelection();\n  }\n\n  Future<void> _onClipboardAction() async {\n    if (_terminalController.selection != null) {\n      final selectedText = _termKey.currentState?.renderTerminal.selectedText;\n      if (selectedText != null && selectedText.isNotEmpty) {\n        await Clipboard.setData(ClipboardData(text: selectedText));\n        if (!mounted) return;\n        context.showSnackBar(libL10n.success);\n        _terminalController.clearSelection();\n        return;\n      }\n      return;\n    }\n    await _onTerminalPaste();\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  FutureOr<void> afterFirstLayout(BuildContext context) async {\n    await _showHelp();\n    await _initTerminal();\n\n    if (Stores.setting.sshWakeLock.fetch()) WakelockPlus.enable();\n\n    HardwareKeyboard.instance.addHandler(_handleKeyEvent);\n  }\n}\n"
  },
  {
    "path": "lib/view/page/ssh/page/virt_key.dart",
    "content": "part of 'page.dart';\n\nextension _VirtKey on SSHPageState {\n  void _doVirtualKey(VirtKey item, VirtKeyboard virtKeyNotifier) {\n    if (item.func != null) {\n      HapticFeedback.mediumImpact();\n      _doVirtualKeyFunc(item.func!);\n      return;\n    }\n    if (item.key != null) {\n      HapticFeedback.mediumImpact();\n      _doVirtualKeyInput(item.key!, virtKeyNotifier);\n    }\n    final inputRaw = item.inputRaw;\n    if (inputRaw != null) {\n      HapticFeedback.mediumImpact();\n      _terminal.textInput(inputRaw);\n    }\n  }\n\n  void _doVirtualKeyInput(TerminalKey key, VirtKeyboard virtKeyNotifier) {\n    switch (key) {\n      case TerminalKey.control:\n        virtKeyNotifier.setCtrl(!virtKeyNotifier.ctrl);\n        break;\n      case TerminalKey.alt:\n        virtKeyNotifier.setAlt(!virtKeyNotifier.alt);\n        break;\n      case TerminalKey.shift:\n        virtKeyNotifier.setShift(!virtKeyNotifier.shift);\n        break;\n      default:\n        _terminal.keyInput(key);\n        break;\n    }\n  }\n\n  Future<void> _doVirtualKeyFunc(VirtualKeyFunc type) async {\n    switch (type) {\n      case VirtualKeyFunc.toggleIME:\n        _termKey.currentState?.toggleFocus();\n        break;\n      case VirtualKeyFunc.backspace:\n        _terminal.keyInput(TerminalKey.backspace);\n        break;\n      case VirtualKeyFunc.clipboard:\n        await _onClipboardAction();\n        break;\n      case VirtualKeyFunc.snippet:\n        final snippetState = ref.read(snippetProvider);\n        final snippets = await context.showPickWithTagDialog<Snippet>(\n          title: libL10n.snippet,\n          tags: snippetState.tags.vn,\n          itemsBuilder: (e) {\n            if (e == TagSwitcher.kDefaultTag) {\n              return snippetState.snippets;\n            }\n            return snippetState.snippets\n                .where((element) => element.tags?.contains(e) ?? false)\n                .toList();\n          },\n          display: (e) => e.name,\n        );\n        if (snippets == null || snippets.isEmpty) return;\n\n        final snippet = snippets.firstOrNull;\n        if (snippet == null) return;\n        snippet.runInTerm(_terminal, widget.args.spi);\n        break;\n      case VirtualKeyFunc.file:\n        // get $PWD from SSH session with unique markers\n        const marker = 'ServerBoxOutput';\n        const markerEnd = 'ServerBoxEnd';\n        const pwdCommand = 'echo \"$marker:\\$PWD:$markerEnd\"';\n        _terminal.textInput(pwdCommand);\n        _terminal.keyInput(TerminalKey.enter);\n\n        // Wait for output with timeout\n        String? initPath;\n        await Future.delayed(const Duration(milliseconds: 700));\n        final startTime = DateTime.now();\n        final timeout = const Duration(seconds: 3);\n\n        while (initPath == null) {\n          // Check if we've exceeded timeout\n          if (DateTime.now().difference(startTime) > timeout) {\n            contextSafe?.showRoundDialog(title: libL10n.error, child: Text(libL10n.empty));\n            return;\n          }\n\n          // Search for marked output in terminal buffer\n          final cmds = _terminal.buffer.lines.toList();\n          for (final line in cmds.reversed) {\n            final lineStr = line.toString();\n            if (lineStr.contains(marker) && lineStr.contains(markerEnd)) {\n              // Extract path between markers\n              final start = lineStr.indexOf(marker) + marker.length + 1; // +1 for ':'\n              final end = lineStr.indexOf(markerEnd) - 1; // -1 for ':'\n              if (start < end) {\n                initPath = lineStr.substring(start, end);\n                if (initPath.isEmpty || initPath == '\\$PWD') {\n                  initPath = null;\n                } else {\n                  break;\n                }\n              }\n            }\n          }\n\n          // Short wait before checking again\n          await Future.delayed(const Duration(milliseconds: 100));\n        }\n\n        if (!initPath.startsWith('/')) {\n          context.showRoundDialog(title: libL10n.error, child: Text('${l10n.remotePath}: $initPath'));\n          return;\n        }\n\n        final args = SftpPageArgs(spi: widget.args.spi, initPath: initPath);\n        SftpPage.route.go(context, args);\n        break;\n    }\n  }\n\n  void _initVirtKeys() {\n    final virtKeys = VirtKeyX.loadFromStore();\n    for (int len = 0; len < virtKeys.length; len += 7) {\n      if (len + 7 > virtKeys.length) {\n        _virtKeysList.add(virtKeys.sublist(len));\n      } else {\n        _virtKeysList.add(virtKeys.sublist(len, len + 7));\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/view/page/ssh/tab.dart",
    "content": "import 'dart:math';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/view/page/server/edit/edit.dart';\nimport 'package:server_box/view/page/ssh/page/page.dart';\n\nclass SSHTabPage extends ConsumerStatefulWidget {\n  const SSHTabPage({super.key});\n\n  @override\n  ConsumerState<SSHTabPage> createState() => _SSHTabPageState();\n\n  static const route = AppRouteNoArg(page: SSHTabPage.new, path: '/ssh');\n}\n\ntypedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;\n\nclass _SSHTabPageState extends ConsumerState<SSHTabPage>\n    with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {\n  late final _TabMap _tabMap = {libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null)};\n  final _pageCtrl = PageController();\n  final _fabVN = 0.vn;\n  final _tabRN = RNode();\n\n  @override\n  void dispose() {\n    super.dispose();\n    _pageCtrl.dispose();\n    _tabRN.dispose();\n    _fabVN.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return Scaffold(\n      appBar: PreferredSizeListenBuilder(\n        listenable: _tabRN,\n        builder: () {\n          return _TabBar(idxVN: _fabVN, map: _tabMap, onTap: _onTapTab, onClose: _onTapClose);\n        },\n      ),\n      body: _buildBody(),\n      floatingActionButton: ValBuilder(\n        listenable: _fabVN,\n        builder: (idx) {\n          if (idx != 0) return const SizedBox();\n          return FloatingActionButton(\n            heroTag: 'sshAddServer',\n            onPressed: () => ServerEditPage.route.go(context),\n            tooltip: libL10n.add,\n            child: const Icon(Icons.add),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildBody() {\n    return ListenBuilder(\n      listenable: _tabRN,\n      builder: () {\n        return PageView.builder(\n          physics: const NeverScrollableScrollPhysics(),\n          controller: _pageCtrl,\n          itemCount: _tabMap.length,\n          itemBuilder: (_, idx) {\n            final name = _tabMap.keys.elementAt(idx);\n            return _tabMap[name]?.page ?? UIs.placeholder;\n          },\n          onPageChanged: (value) => _fabVN.value = value,\n        );\n      },\n    );\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n}\n\nextension on _SSHTabPageState {\n  void _onTapInitCard(Spi spi) async {\n    final name = () {\n      final reg = RegExp('${spi.name}\\\\((\\\\d+)\\\\)');\n      final idxs = _tabMap.keys.map((e) => reg.firstMatch(e)).map((e) => e?.group(1)).whereType<String>();\n      if (idxs.isEmpty) {\n        return _tabMap.keys.contains(spi.name) ? '${spi.name}(1)' : spi.name;\n      }\n      final biggest = idxs.reduce((a, b) => a.length > b.length ? a : b);\n      final biggestInt = int.tryParse(biggest);\n      if (biggestInt != null && biggestInt > 0) {\n        return '${spi.name}(${biggestInt + 1})';\n      }\n      return spi.name;\n    }();\n    final key = Key(name);\n    final args = SshPageArgs(\n      spi: spi,\n      notFromTab: false,\n      onSessionEnd: () {\n        _tabMap.remove(name);\n      },\n    );\n    _tabMap[name] = (\n      page: SSHPage(\n        key: key, // Keep it, or the Flutter will works unexpectedly\n        args: args,\n      ),\n      focus: FocusNode(),\n    );\n    _tabRN.notify();\n    // Wait for the page to be built\n    await Future.delayed(Durations.short3);\n    final idx = _tabMap.keys.toList().indexOf(name);\n    await _toPage(idx);\n  }\n\n  Future<void> _toPage(int idx) async {\n    await _pageCtrl.animateToPage(idx, duration: Durations.short3, curve: Curves.fastEaseInToSlowEaseOut);\n    final focus = _tabMap.values.elementAt(idx).focus;\n    if (focus != null) {\n      FocusScope.of(context).requestFocus(focus);\n    }\n  }\n\n  void _onTapTab(int idx) async {\n    await _toPage(idx);\n  }\n\n  void _onTapClose(String name) async {\n    final confirm = await contextSafe?.showRoundDialog(\n      title: libL10n.attention,\n      child: Text('${libL10n.close} SSH ${libL10n.conn}($name) ?'),\n      actions: Btnx.okReds,\n    );\n    Future.delayed(Durations.short1, FocusScope.of(context).unfocus);\n    if (confirm != true) return;\n\n    _tabMap.remove(name);\n    _tabRN.notify();\n    _pageCtrl.previousPage(duration: Durations.medium1, curve: Curves.fastEaseInToSlowEaseOut);\n  }\n}\n\nfinal class _TabBar extends StatelessWidget implements PreferredSizeWidget {\n  const _TabBar({required this.idxVN, required this.map, required this.onTap, required this.onClose});\n\n  final ValueListenable<int> idxVN;\n  final _TabMap map;\n  final void Function(int idx) onTap;\n  final void Function(String name) onClose;\n\n  List<String> get names => map.keys.toList();\n\n  @override\n  Size get preferredSize => const Size.fromHeight(48);\n\n  @override\n  Widget build(BuildContext context) {\n    return ListenBuilder(\n      listenable: idxVN,\n      builder: () {\n        return ListView.separated(\n          scrollDirection: Axis.horizontal,\n          padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 5),\n          itemCount: names.length,\n          itemBuilder: (_, idx) => _buildItem(idx),\n          separatorBuilder: (_, _) => Padding(\n            padding: const EdgeInsets.symmetric(vertical: 17),\n            child: Container(color: const Color.fromARGB(61, 158, 158, 158), width: 3),\n          ),\n        );\n      },\n    );\n  }\n\n  static const kWideWidth = 90.0;\n  static const kNarrowWidth = 60.0;\n\n  Widget _buildItem(int idx) {\n    final name = names[idx];\n    final selected = idxVN.value == idx;\n    final color = selected ? null : Colors.grey;\n\n    final Widget child;\n    if (idx == 0) {\n      child = Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 13),\n        child: Icon(MingCute.add_circle_fill, size: 17, color: color),\n      );\n    } else {\n      final text = Text(\n        name,\n        maxLines: 1,\n        overflow: TextOverflow.ellipsis,\n        style: TextStyle(color: color),\n        softWrap: false,\n        textAlign: TextAlign.right,\n        textWidthBasis: TextWidthBasis.parent,\n      );\n      final Widget btn;\n      if (selected) {\n        btn = Row(\n          mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n          children: [\n            Btn.icon(\n              icon: Icon(MingCute.close_circle_fill, color: color, size: 17),\n              onTap: () => onClose(name),\n              padding: null,\n            ),\n            SizedBox(width: kNarrowWidth - 15, child: text),\n          ],\n        );\n      } else {\n        btn = Center(child: text);\n      }\n      child = AnimatedContainer(\n        width: selected ? kWideWidth : kNarrowWidth,\n        duration: Durations.medium3,\n        curve: Curves.fastEaseInToSlowEaseOut,\n        child: OverflowBox(maxWidth: selected ? kWideWidth : null, child: btn),\n      );\n    }\n\n    return InkWell(\n      borderRadius: BorderRadius.circular(13),\n      onTap: () => onTap(idx),\n      child: child,\n    ).paddingSymmetric(horizontal: 7);\n  }\n}\n\nclass _AddPage extends ConsumerWidget {\n  const _AddPage({required this.onTapInitCard});\n\n  final void Function(Spi spi) onTapInitCard;\n\n  Widget get _placeholder => const Expanded(child: UIs.placeholder);\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    const viewPadding = 7.0;\n    final viewWidth = context.windowSize.width - 2 * viewPadding;\n\n    final serverState = ref.watch(serversProvider);\n    final itemCount = serverState.servers.length;\n    const itemPadding = 1.0;\n    const itemWidth = 150.0;\n    const itemHeight = 50.0;\n\n    final visualCrossCount = viewWidth / itemWidth;\n    final crossCount = max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1);\n    final mainCount = itemCount ~/ crossCount + 1;\n\n    final order = serverState.serverOrder;\n    \n    if (order.isEmpty) {\n      return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));\n    }\n\n    // Custom grid\n    return ListView(\n      padding: const EdgeInsets.all(viewPadding),\n      children: List.generate(\n        mainCount,\n        (rowIndex) => Row(\n          children: List.generate(crossCount, (columnIndex) {\n            final idx = rowIndex * crossCount + columnIndex;\n            final id = order.elementAtOrNull(idx);\n            final spi = serverState.servers[id];\n            if (spi == null) return _placeholder;\n\n            return Expanded(\n              child: Padding(\n                padding: const EdgeInsets.all(itemPadding),\n                child: InkWell(\n                  onTap: () => onTapInitCard(spi),\n                  child: Container(\n                    height: itemHeight,\n                    alignment: Alignment.centerLeft,\n                    padding: const EdgeInsets.only(left: 17, right: 7),\n                    child: Row(\n                      children: [\n                        Expanded(\n                          child: Text(\n                            spi.name,\n                            style: UIs.text18,\n                            maxLines: 1,\n                            overflow: TextOverflow.ellipsis,\n                          ),\n                        ),\n                        const Icon(Icons.chevron_right),\n                      ],\n                    ),\n                  ),\n                ).cardx,\n              ),\n            );\n          }),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/storage/local.dart",
    "content": "import 'dart:io';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/utils/host_key_helper.dart';\nimport 'package:server_box/data/model/app/path_with_prefix.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/sftp/worker.dart';\nimport 'package:server_box/data/provider/server/all.dart';\nimport 'package:server_box/data/provider/sftp.dart';\nimport 'package:server_box/data/res/misc.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/view/page/storage/sftp.dart';\nimport 'package:server_box/view/page/storage/sftp_mission.dart';\n\nfinal class LocalFilePageArgs {\n  final bool? isPickFile;\n  final String? initDir;\n  const LocalFilePageArgs({this.isPickFile, this.initDir});\n}\n\nclass LocalFilePage extends ConsumerStatefulWidget {\n  final LocalFilePageArgs? args;\n\n  const LocalFilePage({super.key, this.args});\n\n  static const route = AppRoute<String, LocalFilePageArgs>(page: LocalFilePage.new, path: '/files/local');\n\n  @override\n  ConsumerState<LocalFilePage> createState() => _LocalFilePageState();\n}\n\nclass _LocalFilePageState extends ConsumerState<LocalFilePage> with AutomaticKeepAliveClientMixin {\n  late final _path = LocalPath(widget.args?.initDir ?? Paths.file);\n  final _sortType = _SortType.name.vn;\n  late Future<List<(FileSystemEntity, FileStat)>> _entitiesFuture = _getEntities();\n  bool get isPickFile => widget.args?.isPickFile ?? false;\n\n  @override\n  void dispose() {\n    super.dispose();\n    _sortType.dispose();\n  }\n\n  Future<void> _refresh() async {\n    setStateSafe(() {\n      _entitiesFuture = _getEntities();\n    });\n    await _entitiesFuture;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    final title = _path.path.fileNameGetter ?? libL10n.file;\n    return Scaffold(\n      appBar: CustomAppBar(\n        title: AnimatedSwitcher(\n          duration: Durations.short3,\n          child: Text(title, key: ValueKey(title)),\n        ),\n        actions: [\n          if (!isPickFile)\n            IconButton(\n              onPressed: () async {\n                final path = await Pfs.pickFilePath();\n                if (path == null) return;\n                final name = path.getFileName() ?? 'imported';\n                final destinationDir = Directory(_path.path);\n                if (!await destinationDir.exists()) {\n                  await destinationDir.create(recursive: true);\n                }\n                await File(path).copy(_path.path.joinPath(name));\n                _refresh();\n              },\n              icon: const Icon(Icons.add),\n            ),\n          if (!isMobile)\n            IconButton(\n              icon: const Icon(Icons.refresh),\n              tooltip: MaterialLocalizations.of(context).refreshIndicatorSemanticLabel,\n              onPressed: _refresh,\n            ),\n          if (!isPickFile) _buildMissionBtn(),\n          _buildSortBtn(),\n        ],\n      ),\n      body: isMobile\n          ? RefreshIndicator(\n              onRefresh: _refresh,\n              child: _sortType.listen(_buildBody),\n            )\n          : _sortType.listen(_buildBody),\n    );\n  }\n\n  Widget _buildBody() {\n    return FutureWidget(\n      future: _entitiesFuture,\n      loading: UIs.placeholder,\n      success: (items) {\n        items ??= [];\n        final len = _path.canBack ? items.length + 1 : items.length;\n        return ListView.builder(\n          itemCount: len,\n          padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 13),\n          itemBuilder: (context, index) {\n            if (index == 0 && _path.canBack) {\n              return ListTile(\n                leading: const Icon(Icons.arrow_back),\n                title: const Text('..'),\n                onTap: () {\n                  _path.update('..');\n                  _refresh();\n                },\n              ).cardx;\n            }\n\n            if (_path.canBack) index--;\n\n            final item = items![index];\n            final file = item.$1;\n            final fileName = file.path.split(Pfs.seperator).last;\n            final stat = item.$2;\n            final isDir = stat.type == FileSystemEntityType.directory;\n\n            return _buildItem(file: file, fileName: fileName, stat: stat, isDir: isDir);\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildItem({\n    required FileSystemEntity file,\n    required String fileName,\n    required FileStat stat,\n    required bool isDir,\n  }) {\n    final isServerFolder = isDir && file.parent.path == Paths.file;\n    String? serverName;\n    if (isServerFolder) {\n      final servers = ref.read(serversProvider).servers;\n      final server = servers[fileName];\n      if (server != null) {\n        serverName = server.name;\n      }\n    }\n\n    return CardX(\n      child: ListTile(\n        leading: isDir ? const Icon(Icons.folder_open) : const Icon(Icons.insert_drive_file),\n        title: Text(serverName ?? fileName),\n        subtitle: isDir\n            ? (serverName != null ? Text(fileName, style: UIs.textGrey) : null)\n            : Text(stat.size.bytes2Str, style: UIs.textGrey),\n        trailing: Text(stat.modified.ymdhms(), style: UIs.textGrey),\n        onLongPress: () {\n          if (isDir) {\n            _showDirActionDialog(file);\n            return;\n          }\n          _showFileActionDialog(file);\n        },\n        onTap: () {\n          if (!isDir) {\n            _showFileActionDialog(file);\n            return;\n          }\n          _path.update(fileName);\n          _refresh();\n        },\n      ),\n    );\n  }\n\n  Widget _buildMissionBtn() {\n    return IconButton(\n      icon: const Icon(Icons.downloading),\n      onPressed: () => SftpMissionPage.route.go(context),\n    );\n  }\n\n  Future<List<(FileSystemEntity, FileStat)>> _getEntities() async {\n    final files = await Directory(_path.path).list().toList();\n    final stats = await Future.wait(files.map((e) async => (e, await e.stat())));\n    stats.sort(_sortType.value.compareTuple);\n    return stats;\n  }\n\n  Widget _buildSortBtn() {\n    return _sortType.listenVal((value) {\n      return PopupMenuButton<_SortType>(\n        icon: const Icon(Icons.sort),\n        itemBuilder: (_) => _SortType.values.map((e) => e.menuItem).toList(),\n        onSelected: (value) {\n          _sortType.value = value;\n        },\n      );\n    });\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n}\n\nextension _Actions on _LocalFilePageState {\n  Future<void> _showDirActionDialog(FileSystemEntity file) async {\n    context.showRoundDialog(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          ListTile(\n            onTap: () {\n              context.pop();\n              _showRenameDialog(file);\n            },\n            title: Text(libL10n.rename),\n            leading: const Icon(Icons.abc),\n          ),\n          ListTile(\n            onTap: () {\n              context.pop();\n              _showDeleteDialog(file);\n            },\n            title: Text(libL10n.delete),\n            leading: const Icon(Icons.delete),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future<void> _showFileActionDialog(FileSystemEntity file) async {\n    final fileName = file.path.split(Pfs.seperator).lastOrNull ?? '';\n    if (isPickFile) {\n      context.showRoundDialog(\n        title: libL10n.file,\n        child: Text(fileName),\n        actions: [\n          Btn.ok(\n            onTap: () {\n              context.pop();\n              context.pop(file.path);\n            },\n          ),\n        ],\n      );\n      return;\n    }\n    context.showRoundDialog(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          if (isMobile)\n            Btn.tile(\n              icon: const Icon(Icons.edit),\n              text: libL10n.edit,\n              onTap: () => _onTapEdit(file, fileName),\n            ),\n          Btn.tile(\n            icon: const Icon(Icons.abc),\n            text: libL10n.rename,\n            onTap: () {\n              context.pop();\n              _showRenameDialog(file);\n            },\n          ),\n          Btn.tile(\n            icon: const Icon(Icons.delete),\n            text: libL10n.delete,\n            onTap: () {\n              context.pop();\n              _showDeleteDialog(file);\n            },\n          ),\n          Btn.tile(\n            icon: const Icon(Icons.upload),\n            text: libL10n.upload,\n            onTap: () => _onTapUpload(file, fileName),\n          ),\n          Btn.tile(\n            icon: const Icon(Icons.open_in_new),\n            text: libL10n.open,\n            onTap: () {\n              Pfs.sharePaths(paths: [file.absolute.path]);\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _showRenameDialog(FileSystemEntity file) {\n    final fileName = file.path.split(Pfs.seperator).last;\n    final ctrl = TextEditingController(text: fileName);\n    void onSubmit() async {\n      final newName = ctrl.text;\n      if (newName.isEmpty) {\n        context.showSnackBar(libL10n.empty);\n        return;\n      }\n\n      context.pop();\n      final newPath = '${file.parent.path}${Pfs.seperator}$newName';\n      await context.showLoadingDialog(fn: () => file.rename(newPath));\n\n      setStateSafe(() {});\n    }\n\n    context.showRoundDialog(\n      title: libL10n.rename,\n      child: Input(\n        autoFocus: true,\n        icon: Icons.abc,\n        label: libL10n.name,\n        controller: ctrl,\n        suggestion: true,\n        maxLines: 3,\n        onSubmitted: (p0) => onSubmit(),\n      ),\n      actions: Btn.ok(onTap: onSubmit).toList,\n    );\n  }\n\n  void _showDeleteDialog(FileSystemEntity file) {\n    final fileName = file.path.split(Pfs.seperator).last;\n    context.showRoundDialog(\n      title: libL10n.delete,\n      child: Text(libL10n.askContinue('${libL10n.delete} $fileName')),\n      actions: Btn.ok(\n        onTap: () async {\n          context.pop();\n          try {\n            await file.delete(recursive: true);\n          } catch (e) {\n            context.showSnackBar('${libL10n.fail}:\\n$e');\n            return;\n          }\n          setStateSafe(() {});\n        },\n      ).toList,\n    );\n  }\n}\n\nextension _OnTapFile on _LocalFilePageState {\n  void _onTapEdit(FileSystemEntity file, String fileName) async {\n    context.pop();\n    final stat = await file.stat();\n    if (stat.size > Miscs.editorMaxSize) {\n      context.showRoundDialog(\n        title: libL10n.attention,\n        child: Text(l10n.fileTooLarge(fileName, stat.size, '1m')),\n      );\n      return;\n    }\n\n    await EditorPage.route.go(\n      context,\n      args: EditorPageArgs(\n        path: file.absolute.path,\n        onSave: (_) {\n          context.showSnackBar(libL10n.saved);\n          setStateSafe(() {});\n        },\n        closeAfterSave: Stores.setting.closeAfterSave.fetch(),\n        softWrap: Stores.setting.editorSoftWrap.fetch(),\n        enableHighlight: Stores.setting.editorHighlight.fetch(),\n        fontFamily: () {\n          final font = Stores.setting.editorFontFamily.fetch();\n          return font.isEmpty ? null : font;\n        }(),\n      ),\n    );\n  }\n\n  void _onTapUpload(FileSystemEntity file, String fileName) async {\n    context.pop();\n\n    final spi = await context.showPickSingleDialog<Spi>(\n      title: libL10n.select,\n      items: ref.read(serversProvider).servers.values.toList(),\n      display: (e) => e.name,\n    );\n    if (spi == null) return;\n\n    final args = SftpPageArgs(spi: spi, isSelect: true);\n    final remotePath = await SftpPage.route.go(context, args);\n    if (remotePath == null) {\n      return;\n    }\n\n    if (!await ensureHostKeyAcceptedForSftp(context, spi)) {\n      return;\n    }\n\n    ref.read(sftpProvider.notifier).add(SftpReq(spi, '$remotePath/$fileName', file.absolute.path, SftpReqType.upload));\n    context.showSnackBar(l10n.added2List);\n  }\n}\n\nenum _SortType {\n  name,\n  size,\n  time;\n\n  int compareTuple((FileSystemEntity, FileStat) a, (FileSystemEntity, FileStat) b) {\n    return switch (this) {\n      _SortType.name => a.$1.path.compareTo(b.$1.path),\n      _SortType.size => a.$2.size.compareTo(b.$2.size),\n      _SortType.time => a.$2.modified.compareTo(b.$2.modified),\n    };\n  }\n\n  String get i18n => switch (this) {\n    name => libL10n.name,\n    size => l10n.size,\n    time => l10n.time,\n  };\n\n  IconData get icon => switch (this) {\n    name => Icons.sort_by_alpha,\n    size => Icons.sort,\n    time => Icons.access_time,\n  };\n\n  PopupMenuItem<_SortType> get menuItem {\n    return PopupMenuItem(\n      value: this,\n      child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [Icon(icon), Text(i18n)]),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/storage/sftp.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:dartssh2/dartssh2.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:icons_plus/icons_plus.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/extension/sftpfile.dart';\nimport 'package:server_box/core/utils/comparator.dart';\nimport 'package:server_box/core/utils/host_key_helper.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/sftp/browser_status.dart';\nimport 'package:server_box/data/model/sftp/worker.dart';\nimport 'package:server_box/data/provider/server/single.dart';\nimport 'package:server_box/data/provider/sftp.dart';\nimport 'package:server_box/data/res/misc.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/view/page/ssh/page/page.dart';\nimport 'package:server_box/view/page/storage/local.dart';\nimport 'package:server_box/view/page/storage/sftp_mission.dart';\nimport 'package:server_box/view/widget/omit_start_text.dart';\nimport 'package:server_box/view/widget/unix_perm.dart';\n\nfinal class SftpPageArgs {\n  final Spi spi;\n  final bool isSelect;\n  final String? initPath;\n\n  const SftpPageArgs({required this.spi, this.isSelect = false, this.initPath});\n}\n\nclass SftpPage extends ConsumerStatefulWidget {\n  final SftpPageArgs args;\n\n  const SftpPage({super.key, required this.args});\n\n  @override\n  ConsumerState<SftpPage> createState() => _SftpPageState();\n\n  static const route = AppRouteArg<String, SftpPageArgs>(page: SftpPage.new, path: '/sftp');\n}\n\nclass _SftpPageState extends ConsumerState<SftpPage> with AfterLayoutMixin {\n  late final SftpBrowserStatus _status;\n  late final SSHClient _client;\n  final _sortOption = _SortOption().vn;\n\n  @override\n  void initState() {\n    super.initState();\n    final serverState = ref.read(serverProvider(widget.args.spi.id));\n    _client = serverState.client!;\n    _status = SftpBrowserStatus(_client);\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    _sortOption.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final children = [\n      Btn.icon(icon: const Icon(Icons.downloading), onTap: () => SftpMissionPage.route.go(context)),\n      _buildSortMenu(),\n      _buildSearchBtn(),\n    ];\n    if (isDesktop) children.add(_buildRefreshBtn());\n\n    return Scaffold(\n      appBar: CustomAppBar(title: Text(widget.args.spi.name), actions: children),\n      body: _buildFileView(),\n      bottomNavigationBar: _buildBottom(),\n    );\n  }\n\n  @override\n  FutureOr<void> afterFirstLayout(BuildContext context) async {\n    String initPath;\n\n    try {\n      final homeResult = await _client.run('eval echo ~${widget.args.spi.user}');\n      final homePath = homeResult.string.trim();\n      if (homePath.isNotEmpty && homePath.startsWith('/')) {\n        initPath = homePath;\n      } else {\n        final user = widget.args.spi.user;\n        initPath = user != 'root' ? '/home/$user' : '/root';\n      }\n    } catch (_) {\n      final user = widget.args.spi.user;\n      initPath = user != 'root' ? '/home/$user' : '/root';\n    }\n\n    if (Stores.setting.sftpOpenLastPath.fetch()) {\n      final history = Stores.history.sftpLastPath.fetch(widget.args.spi.id);\n      if (history != null) {\n        SftpClient? sftp;\n        try {\n          final normalizedHistory = _normalizeSftpPath(history);\n          sftp = await _client.sftp();\n          await sftp.listdir(normalizedHistory);\n          initPath = normalizedHistory;\n        } catch (_) {\n        } finally {\n          sftp?.close();\n        }\n      }\n    }\n\n    _status.path.path = widget.args.initPath ?? initPath;\n    unawaited(_listDir());\n  }\n}\n\nextension _UI on _SftpPageState {\n  Widget _buildSortMenu() {\n    final options = [\n      (_SortType.name, libL10n.name),\n      (_SortType.size, l10n.size),\n      (_SortType.time, l10n.time),\n    ];\n    return _sortOption.listenVal((value) {\n      return PopupMenuButton<_SortType>(\n        icon: const Icon(Icons.sort),\n        itemBuilder: (context) {\n          return options.map((r) {\n            final (type, name) = r;\n            final selected = type == value.sortBy;\n            final title = selected ? \"$name (${value.reversed ? '-' : '+'})\" : name;\n            return PopupMenuItem(\n              value: type,\n              child: Text(\n                title,\n                style: TextStyle(\n                  color: selected ? UIs.primaryColor : null,\n                  fontWeight: selected ? FontWeight.bold : null,\n                ),\n              ),\n            );\n          }).toList();\n        },\n        onSelected: (sortBy) {\n          final old = _sortOption.value;\n          if (old.sortBy == sortBy) {\n            _sortOption.value = old.copyWith(reversed: !old.reversed);\n          } else {\n            _sortOption.value = old.copyWith(sortBy: sortBy);\n          }\n        },\n      );\n    });\n  }\n\n  Widget _buildBottom() {\n    final children = widget.args.isSelect\n        ? [\n            IconButton(onPressed: () => context.pop(_status.path.path), icon: const Icon(Icons.done)),\n            _buildSearchBtn(),\n          ]\n        : [_buildBackBtn(), _buildHomeBtn(), _buildAddBtn(), _buildGotoBtn(), _buildUploadBtn()];\n    return SafeArea(\n      child: Container(\n        padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            OmitStartText(_status.path.path),\n            Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: children),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildFileView() {\n    if (_status.files.isEmpty) return Center(child: Text(libL10n.empty));\n\n    return RefreshIndicator(\n      onRefresh: _listDir,\n      child: FadeIn(\n        key: Key(widget.args.spi.name + _status.path.path),\n        child: ValBuilder(\n          listenable: _sortOption,\n          builder: (sortOption) {\n            final files = sortOption.sortBy.sort(_status.files, reversed: sortOption.reversed);\n            return ListView.builder(\n              itemCount: files.length,\n              padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),\n              itemBuilder: (_, index) => _buildItem(files[index]),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _buildItem(SftpName file, {VoidCallback? beforeTap}) {\n    final isDir = file.attr.isDirectory;\n    final double screenWidth = MediaQuery.sizeOf(context).width;\n    if (screenWidth < 350) {\n      return CardX(\n        child: ListTile(\n          leading: Icon(isDir ? Icons.folder_outlined : Icons.insert_drive_file),\n          title: Text(file.filename),\n          subtitle: isDir ? Text('${_getTime(file.attr.modifyTime)}\\n${file.attr.mode?.str ?? ''}', style: UIs.textGrey) :\n            Text('${(file.attr.size ?? 0).bytes2Str}\\n${_getTime(file.attr.modifyTime)}\\n${file.attr.mode?.str ?? ''}', style: UIs.textGrey),\n          onTap: () {\n            beforeTap?.call();\n            if (isDir) {\n              _status.path.path = file.filename;\n              _listDir();\n            } else {\n              _onItemPress(file, true);\n            }\n          },\n          onLongPress: () {\n            beforeTap?.call();\n            _onItemPress(file, !isDir);\n          },\n        ),\n      );\n    } else {\n      return CardX(\n        child: ListTile(\n          leading: Icon(isDir ? Icons.folder_outlined : Icons.insert_drive_file),\n          title: Text(file.filename),\n          trailing: Text(\n            '${_getTime(file.attr.modifyTime)}\\n${file.attr.mode?.str ?? ''}',\n            style: UIs.textGrey,\n            textAlign: TextAlign.right,\n          ),\n          subtitle: isDir ? null : Text((file.attr.size ?? 0).bytes2Str, style: UIs.textGrey),\n          onTap: () {\n            beforeTap?.call();\n            if (isDir) {\n              _status.path.path = file.filename;\n              _listDir();\n            } else {\n              _onItemPress(file, true);\n            }\n          },\n          onLongPress: () {\n            beforeTap?.call();\n            _onItemPress(file, !isDir);\n          },\n        ),\n      );\n    }\n  }\n}\n\nextension _Actions on _SftpPageState {\n  void _onItemPress(SftpName file, bool notDir) {\n    final children = [\n      ListTile(leading: const Icon(Icons.delete), title: Text(libL10n.delete), onTap: () => _delete(file)),\n      ListTile(leading: const Icon(Icons.abc), title: Text(libL10n.rename), onTap: () => _rename(file)),\n      ListTile(\n        leading: const Icon(MingCute.copy_line),\n        title: Text(l10n.copyPath),\n        onTap: () {\n          context.pop();\n          Pfs.copy(_getRemotePath(file));\n          context.showSnackBar(libL10n.success);\n        },\n      ),\n      ListTile(\n        leading: const Icon(Icons.security),\n        title: Text(l10n.permission),\n        onTap: () async {\n          context.pop();\n\n          final perm = file.attr.mode?.toUnixPerm() ?? UnixPerm.empty;\n          var newPerm = perm.copyWith();\n          final ok = await context.showRoundDialog(\n            child: UnixPermEditor(perm: perm, onChanged: (p) => newPerm = p),\n            actions: Btnx.okReds,\n          );\n\n          final permStr = newPerm.perm;\n          if (ok == true && permStr != perm.perm) {\n            await context.showLoadingDialog(\n              fn: () async {\n                await _client.run('chmod $permStr \"${_getRemotePath(file)}\"');\n                await _listDir();\n              },\n            );\n          }\n        },\n      ),\n    ];\n    if (notDir) {\n      children.addAll([\n        ListTile(leading: const Icon(Icons.edit), title: Text(libL10n.edit), onTap: () => _edit(file)),\n        ListTile(\n          leading: const Icon(Icons.download),\n          title: Text(libL10n.download),\n          onTap: () => _download(file),\n        ),\n        // Only show decompress option when the file is a compressed file\n        if (_canDecompress(file.filename))\n          ListTile(\n            leading: const Icon(Icons.folder_zip),\n            title: Text(libL10n.decompress),\n            onTap: () => _decompress(file),\n          ),\n      ]);\n    }\n    context.showRoundDialog(\n      child: Column(mainAxisSize: MainAxisSize.min, children: children),\n    );\n  }\n\n  Future<void> _edit(SftpName name) async {\n    context.pop();\n\n    // #489\n    final editor = Stores.setting.sftpEditor.fetch();\n    if (editor.isNotEmpty) {\n      // Use single quote to avoid escape\n      final cmd = \"$editor '${_getRemotePath(name)}'\";\n      final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd);\n      await SSHPage.route.go(context, args);\n      await _listDir();\n      return;\n    }\n\n    final size = name.attr.size;\n    if (size == null || size > Miscs.editorMaxSize) {\n      context.showSnackBar(l10n.fileTooLarge(name.filename, size ?? 0, Miscs.editorMaxSize));\n      return;\n    }\n\n    if (!await ensureHostKeyAcceptedForSftp(context, widget.args.spi)) {\n      return;\n    }\n\n    final remotePath = _getRemotePath(name);\n    final localPath = _getLocalPath(remotePath);\n    final completer = Completer();\n    final req = SftpReq(widget.args.spi, remotePath, localPath, SftpReqType.download);\n    ref.read(sftpProvider.notifier).add(req, completer: completer);\n    final (suc, err) = await context.showLoadingDialog(fn: () => completer.future);\n    if (suc == null || err != null) return;\n\n    await EditorPage.route.go(\n      context,\n      args: EditorPageArgs(\n        path: localPath,\n        onSave: (_) async {\n          if (!await ensureHostKeyAcceptedForSftp(context, req.spi)) {\n            return;\n          }\n          ref\n              .read(sftpProvider.notifier)\n              .add(SftpReq(req.spi, remotePath, localPath, SftpReqType.upload));\n          context.showSnackBar(l10n.added2List);\n        },\n        closeAfterSave: Stores.setting.closeAfterSave.fetch(),\n        softWrap: Stores.setting.editorSoftWrap.fetch(),\n        enableHighlight: Stores.setting.editorHighlight.fetch(),\n        fontFamily: () {\n          final font = Stores.setting.editorFontFamily.fetch();\n          return font.isEmpty ? null : font;\n        }(),\n      ),\n    );\n  }\n\n  void _download(SftpName name) {\n    context.showRoundDialog(\n      title: libL10n.attention,\n      child: Text('${l10n.dl2Local(name.filename)}\\n${l10n.keepForeground}'),\n      actions: [\n        TextButton(onPressed: () => context.pop(), child: Text(libL10n.cancel)),\n        TextButton(\n          onPressed: () async {\n            context.pop();\n            final remotePath = _getRemotePath(name);\n\n            if (!await ensureHostKeyAcceptedForSftp(context, widget.args.spi)) {\n              return;\n            }\n\n            ref\n                .read(sftpProvider.notifier)\n                .add(SftpReq(widget.args.spi, remotePath, _getLocalPath(remotePath), SftpReqType.download));\n\n            context.pop();\n          },\n          child: Text(libL10n.download),\n        ),\n      ],\n    );\n  }\n\n  void _delete(SftpName file) {\n    context.pop();\n    final isDir = file.attr.isDirectory;\n    var useRmr = Stores.setting.sftpRmrDir.fetch();\n    final text = () {\n      if (isDir && !useRmr) {\n        return libL10n.askContinue('${libL10n.delete} ${file.filename}');\n      }\n      return libL10n.askContinue('${libL10n.delete} ${file.filename}');\n    }();\n\n    // Most users don't know that SFTP can't delete a directory which is not\n    // empty, so we provide a checkbox to let user choose to use `rm -r` or not\n    context.showRoundDialog(\n      title: libL10n.attention,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          ListTile(title: Text(text)),\n          if (!useRmr)\n            StatefulBuilder(\n              builder: (_, setState) {\n                return CheckboxListTile(\n                  title: Text(l10n.sftpRmrDirSummary),\n                  value: useRmr,\n                  onChanged: (val) {\n                    setState(() {\n                      useRmr = val ?? false;\n                    });\n                  },\n                );\n              },\n            ),\n        ],\n      ),\n      actions: [\n        TextButton(onPressed: () => context.pop(), child: Text(libL10n.cancel)),\n        TextButton(\n          onPressed: () async {\n            context.pop();\n\n            final (suc, err) = await context.showLoadingDialog(\n              fn: () async {\n                final remotePath = _getRemotePath(file);\n                if (useRmr) {\n                  await _client.run('rm -r \"$remotePath\"');\n                } else if (file.attr.isDirectory) {\n                  await _status.client!.rmdir(remotePath);\n                } else {\n                  await _status.client!.remove(remotePath);\n                }\n                return true;\n              },\n            );\n            if (suc == null || err != null) return;\n\n            _listDir();\n          },\n          child: Text(libL10n.delete, style: UIs.textRed),\n        ),\n      ],\n    );\n  }\n\n  void _mkdir() {\n    context.pop();\n    final textController = TextEditingController();\n\n    void onSubmitted() async {\n      final text = textController.text.trim();\n      if (text.isEmpty) {\n        context.showRoundDialog(child: Text(libL10n.empty), actions: Btnx.oks);\n        return;\n      }\n      context.pop();\n\n      final (suc, err) = await context.showLoadingDialog(\n        fn: () async {\n          final dir = '${_status.path.path}/$text';\n          await _status.client!.mkdir(dir);\n          return true;\n        },\n      );\n      if (suc == null || err != null) return;\n\n      _listDir();\n    }\n\n    context.showRoundDialog(\n      title: libL10n.folder,\n      child: Input(\n        autoFocus: true,\n        icon: Icons.folder,\n        controller: textController,\n        label: libL10n.name,\n        suggestion: true,\n        onSubmitted: (_) => onSubmitted(),\n      ),\n      actions: Btn.ok(onTap: onSubmitted, red: true).toList,\n    );\n  }\n\n  void _newFile() {\n    context.pop();\n    final textController = TextEditingController();\n\n    void onSubmitted() async {\n      final text = textController.text.trim();\n      if (text.isEmpty) {\n        context.showRoundDialog(title: libL10n.attention, child: Text(libL10n.empty), actions: Btnx.oks);\n        return;\n      }\n      context.pop();\n\n      final (suc, err) = await context.showLoadingDialog(\n        fn: () async {\n          final path = '${_status.path.path}/$text';\n          await _client.run('touch \"$path\"');\n          return true;\n        },\n      );\n      if (suc == null || err != null) return;\n\n      _listDir();\n    }\n\n    context.showRoundDialog(\n      title: libL10n.file,\n      child: Input(\n        autoFocus: true,\n        icon: Icons.insert_drive_file,\n        controller: textController,\n        label: libL10n.name,\n        suggestion: true,\n        onSubmitted: (_) => onSubmitted(),\n      ),\n      actions: Btn.ok(onTap: onSubmitted, red: true).toList,\n    );\n  }\n\n  void _rename(SftpName file) {\n    context.pop();\n    final textController = TextEditingController(text: file.filename);\n\n    void onSubmitted() async {\n      final text = textController.text.trim();\n      if (text.isEmpty) {\n        context.showRoundDialog(title: libL10n.attention, child: Text(libL10n.empty), actions: Btnx.oks);\n        return;\n      }\n      context.pop();\n\n      final (suc, err) = await context.showLoadingDialog(\n        fn: () async {\n          final newName = textController.text;\n          await _status.client?.rename(file.filename, newName);\n          return true;\n        },\n      );\n      if (suc == null || err != null) return;\n\n      _listDir();\n    }\n\n    context.showRoundDialog(\n      title: libL10n.rename,\n      child: Input(\n        autoFocus: true,\n        icon: Icons.abc,\n        controller: textController,\n        label: libL10n.name,\n        suggestion: true,\n        onSubmitted: (_) => onSubmitted(),\n      ),\n      actions: [\n        Btn.cancel(),\n        Btn.ok(onTap: onSubmitted, red: true),\n      ],\n    );\n  }\n\n  Future<void> _decompress(SftpName name) async {\n    context.pop();\n    final absPath = _getRemotePath(name);\n    final cmd = _getDecompressCmd(absPath);\n    if (cmd == null) {\n      context.showRoundDialog(\n        title: libL10n.error,\n        child: Text('Unsupport file: ${name.filename}'),\n        actions: [Btn.ok()],\n      );\n      return;\n    }\n\n    final confirm = await context.showRoundDialog(\n      title: libL10n.attention,\n      child: SimpleMarkdown(data: '```sh\\n$cmd\\n```'),\n      actions: Btnx.cancelRedOk,\n    );\n    if (confirm != true) return;\n\n    final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd);\n    await SSHPage.route.go(context, args);\n    _listDir();\n  }\n\n  String _getRemotePath(SftpName name) {\n    final prePath = _status.path.path;\n    // Only support Linux as remote now, so the seperator is '/'\n    return prePath.joinPath(name.filename, separator: '/');\n  }\n\n  /// Local file dir + server id + remote path\n  String _getLocalPath(String remotePath) {\n    var normalizedPath = remotePath.replaceAll('/', Pfs.seperator);\n    if (normalizedPath.startsWith(Pfs.seperator)) {\n      normalizedPath = normalizedPath.substring(1);\n    }\n    return Paths.file.joinPath(widget.args.spi.id).joinPath(normalizedPath);\n  }\n\n  /// Only return true if the path is changed\n  Future<bool?> _listDir() async {\n    final (ret, err) = await context.showLoadingDialog(\n      fn: () async {\n        _status.client ??= await _client.sftp();\n        final listPath = _status.path.path;\n        final fs = await _status.client?.listdir(listPath);\n        if (fs == null) {\n          return false;\n        }\n        fs.sort((a, b) => a.filename.compareTo(b.filename));\n\n        /// Issue #97\n        /// In order to compatible with the Synology NAS\n        /// which not has '.' and '..' in listdir\n        if (fs.firstOrNull?.filename == '.') {\n          fs.removeAt(0);\n        }\n\n        if (fs.isNotEmpty && fs.firstOrNull?.filename == '..' && _status.path.path == '/') {\n          fs.removeAt(0);\n        }\n        if (mounted) {\n          // ignore: invalid_use_of_protected_member\n          setState(() {\n            _status.files\n              ..clear()\n              ..addAll(fs);\n          });\n\n          // Only update history when success\n          if (Stores.setting.sftpOpenLastPath.fetch()) {\n            final normalizedPath = _normalizeSftpPath(listPath);\n            Stores.history.sftpLastPath.put(widget.args.spi.id, normalizedPath);\n          }\n\n          return true;\n        }\n        return false;\n      },\n      barrierDismiss: true,\n    );\n    return ret ?? err == null;\n  }\n\n  Future<void> _backward() async {\n    if (_status.path.undo()) {\n      await _listDir();\n    }\n  }\n\n  Widget _buildBackBtn() {\n    return Btn.icon(onTap: _backward, icon: const Icon(Icons.arrow_back));\n  }\n\n  Widget _buildSearchBtn() {\n    return Btn.icon(\n      onTap: () {\n        Stream<SftpName> find(String query) async* {\n          final fs = _status.files;\n          for (final f in fs) {\n            if (f.filename.contains(query)) yield f;\n          }\n        }\n\n        showSearch(\n          context: context,\n          delegate: SearchPage(\n            padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),\n            future: (q) => find(q).toList(),\n            builder: (ctx, e) => _buildItem(e, beforeTap: ctx.pop),\n          ),\n        );\n      },\n      icon: const Icon(Icons.search),\n    );\n  }\n\n  Widget _buildUploadBtn() {\n    return Btn.icon(\n      onTap: () async {\n        final idx = await context.showRoundDialog(\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              Btn.tile(icon: const Icon(Icons.open_in_new), text: l10n.system, onTap: () => context.pop(1)),\n              Btn.tile(icon: const Icon(Icons.folder), text: libL10n.inner, onTap: () => context.pop(0)),\n            ],\n          ),\n        );\n        final path = switch (idx) {\n          0 => await LocalFilePage.route.go(context, args: const LocalFilePageArgs(isPickFile: true)),\n          1 => await Pfs.pickFilePath(),\n          _ => null,\n        };\n        if (path == null) return;\n\n        final remoteDir = _status.path.path;\n        final fileName = path.split(Platform.pathSeparator).lastOrNull;\n        final remotePath = '$remoteDir/$fileName';\n        Loggers.app.info('SFTP upload local: $path, remote: $remotePath');\n        if (!await ensureHostKeyAcceptedForSftp(context, widget.args.spi)) {\n          return;\n        }\n        ref\n            .read(sftpProvider.notifier)\n            .add(SftpReq(widget.args.spi, remotePath, path, SftpReqType.upload));\n      },\n      icon: const Icon(Icons.upload_file),\n    );\n  }\n\n  Widget _buildAddBtn() {\n    return Btn.icon(\n      onTap: () => context.showRoundDialog(\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            Btn.tile(icon: const Icon(Icons.folder), text: libL10n.folder, onTap: _mkdir),\n            Btn.tile(icon: const Icon(Icons.insert_drive_file), text: libL10n.file, onTap: _newFile),\n          ],\n        ),\n      ),\n      icon: const Icon(Icons.add),\n    );\n  }\n\n  Widget _buildGotoBtn() {\n    return Btn.icon(\n      onTap: () async {\n        final p = await context.showRoundDialog<String>(\n          title: l10n.goto,\n          child: Autocomplete<String>(\n            optionsBuilder: (val) {\n              if (!Stores.setting.recordHistory.fetch()) {\n                return [];\n              }\n              return Stores.history.sftpGoPath.all.cast<String>().where((e) => e.contains(val.text));\n            },\n            fieldViewBuilder: (_, controller, node, _) {\n              return Input(\n                autoFocus: true,\n                icon: Icons.abc,\n                label: libL10n.path,\n                node: node,\n                controller: controller,\n                suggestion: true,\n                onSubmitted: (value) => context.pop(value),\n              );\n            },\n          ),\n        );\n\n        if (p == null || p.isEmpty) {\n          return;\n        }\n\n        _status.path.path = p;\n        final suc = await _listDir() ?? false;\n        if (suc && Stores.setting.recordHistory.fetch()) {\n          Stores.history.sftpGoPath.add(p);\n        }\n      },\n      icon: const Icon(Icons.gps_fixed),\n    );\n  }\n\n  Widget _buildRefreshBtn() {\n    return Btn.icon(onTap: _listDir, icon: const Icon(Icons.refresh));\n  }\n\n  Widget _buildHomeBtn() {\n    return IconButton(\n      onPressed: () {\n        final user = widget.args.spi.user;\n        _status.path.path = user != 'root' ? '/home/$user' : '/root';\n        _listDir();\n      },\n      icon: const Icon(Icons.home),\n    );\n  }\n}\n\nString _normalizeSftpPath(String path) => path.replaceAll(RegExp(r'/+'), '/');\n\nString? _getDecompressCmd(String filename) {\n  for (final ext in _extCmdMap.keys) {\n    if (filename.endsWith('.$ext')) {\n      return _extCmdMap[ext]?.replaceAll('FILE', '\"$filename\"');\n    }\n  }\n  return null;\n}\n\nbool _canDecompress(String filename) {\n  for (final ext in _extCmdMap.keys) {\n    if (filename.endsWith('.$ext')) {\n      return true;\n    }\n  }\n  return false;\n}\n\n/// Translate from\n/// https://github.com/ohmyzsh/ohmyzsh/blob/03a0d5bbaedc732436b5c67b166cde954817cc2f/plugins/extract/extract.plugin.zsh\nconst _extCmdMap = {\n  'tar.gz': 'tar zxvf FILE',\n  'tgz': 'tar zxvf FILE',\n  'tar.bz2': 'tar jxvf FILE',\n  'tbz2': 'tar jxvf FILE',\n  'tar.xz': 'tar --xz -xvf FILE',\n  'txz': 'tar --xz -xvf FILE',\n  'tar.lzma': 'tar --lzma -xvf FILE',\n  'tlz': 'tar --lzma -xvf FILE',\n  'tar.zst': 'tar --zstd -xvf FILE',\n  'tzst': 'tar --zstd -xvf FILE',\n  'tar': 'tar xvf FILE',\n  'tar.lz': 'tar xvf FILE',\n  'tar.lz4': 'lz4 -c -d FILE | tar xvf - ',\n  'gz': 'gunzip FILE',\n  'bz2': 'bunzip2 FILE',\n  'xz': 'unxz FILE',\n  'lzma': 'unlzma FILE',\n  'z': 'uncompress FILE',\n  'zip': 'unzip FILE',\n  'war': 'unzip FILE',\n  'jar': 'unzip FILE',\n  'ear': 'unzip FILE',\n  'sublime-package': 'unzip FILE',\n  'ipa': 'unzip FILE',\n  'ipsw': 'unzip FILE',\n  'apk': 'unzip FILE',\n  'xpi': 'unzip FILE',\n  'aar': 'unzip FILE',\n  'whl': 'unzip FILE',\n  'rar': 'unrar x -ad FILE',\n  'rpm': 'rpm2cpio FILE | cpio --quiet -id',\n  '7z': '7za x FILE',\n  // 'deb': 'mkdir -p \"control\" \"data\"'\n  //     'ar vx FILE > /dev/null'\n  //     'cd control; extract ../control.tar.*'\n  //     'cd ../data; extract ../data.tar.*'\n  //     'cd ..; rm *.tar.* debian-binary',\n  'zst': 'unzstd FILE',\n  'cab': 'cabextract FILE',\n  'exe': 'cabextract FILE',\n  'cpio': 'cpio -idmvF FILE',\n  'obscpio': 'cpio -idmvF FILE',\n  'zpaq': 'zpaq x FILE',\n};\n\n/// Return fmt: 2021-01-01 00:00:00\nString _getTime(int? unixMill) {\n  return DateTime.fromMillisecondsSinceEpoch((unixMill ?? 0) * 1000).toString().replaceFirst('.000', '');\n}\n\nenum _SortType {\n  name,\n  time,\n  size;\n\n  List<SftpName> sort(List<SftpName> files, {bool reversed = false}) {\n    var comparator = ChainComparator<SftpName>.create();\n    if (Stores.setting.sftpShowFoldersFirst.fetch()) {\n      comparator = comparator.thenTrueFirst((x) => x.attr.isDirectory);\n    }\n    switch (this) {\n      case _SortType.name:\n        files.sort(\n          comparator\n              .thenWithComparator(\n                (a, b) => Comparators.compareStringCaseInsensitive()(a.filename, b.filename),\n                reversed: reversed,\n              )\n              .compare,\n        );\n        break;\n      case _SortType.time:\n        files.sort(comparator.thenCompareBy<num>((x) => x.attr.modifyTime ?? 0, reversed: reversed).compare);\n        break;\n      case _SortType.size:\n        files.sort(comparator.thenCompareBy<num>((x) => x.attr.size ?? 0, reversed: reversed).compare);\n        break;\n    }\n    return files;\n  }\n}\n\nclass _SortOption {\n  final _SortType sortBy;\n  final bool reversed;\n\n  _SortOption({this.sortBy = _SortType.name, this.reversed = false});\n\n  _SortOption copyWith({_SortType? sortBy, bool? reversed}) {\n    return _SortOption(sortBy: sortBy ?? this.sortBy, reversed: reversed ?? this.reversed);\n  }\n}\n"
  },
  {
    "path": "lib/view/page/storage/sftp_mission.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/data/model/sftp/worker.dart';\nimport 'package:server_box/data/provider/sftp.dart';\nimport 'package:server_box/view/page/storage/local.dart';\n\nclass SftpMissionPage extends ConsumerStatefulWidget {\n  const SftpMissionPage({super.key});\n\n  @override\n  ConsumerState<SftpMissionPage> createState() => _SftpMissionPageState();\n\n  static const route = AppRouteNoArg(page: SftpMissionPage.new, path: '/sftp/mission');\n}\n\nclass _SftpMissionPageState extends ConsumerState<SftpMissionPage> {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(title: Text(libL10n.mission, style: UIs.text18)),\n      body: _buildBody(),\n    );\n  }\n\n  Widget _buildBody() {\n    final status = ref.watch(sftpProvider.select((pro) => pro.requests));\n    if (status.isEmpty) {\n      return Center(child: Text(libL10n.empty));\n    }\n    return ListView.builder(\n      padding: const EdgeInsets.all(11),\n      itemCount: status.length,\n      itemBuilder: (context, index) {\n        return _buildItem(status[index]);\n      },\n    );\n  }\n\n  Widget _buildItem(SftpReqStatus status) {\n    final err = status.error;\n    if (err != null) {\n      return _wrapInCard(\n        status: status,\n        subtitle: libL10n.error,\n        trailing: IconButton(\n          onPressed: () => context.showRoundDialog(title: libL10n.error, child: Text(err.toString())),\n          icon: const Icon(Icons.error),\n        ),\n      );\n    }\n    return switch (status.status) {\n      const (SftpWorkerStatus.finished) => _buildFinished(status),\n      const (SftpWorkerStatus.loading) => _buildLoading(status),\n      const (SftpWorkerStatus.sshConnectted) => _buildConnected(status),\n      const (SftpWorkerStatus.preparing) => _buildPreparing(status),\n      _ => _buildDefault(status),\n    };\n  }\n\n  Widget _buildPreparing(SftpReqStatus status) {\n    return _wrapInCard(\n      status: status,\n      subtitle: l10n.sftpDlPrepare,\n      trailing: _buildDelete(status.fileName, status.id),\n    );\n  }\n\n  Widget _buildDefault(SftpReqStatus status) {\n    return _wrapInCard(\n      status: status,\n      subtitle: l10n.unknown,\n      trailing: IconButton(\n        onPressed: () => context.showRoundDialog(\n          title: libL10n.error,\n          child: Text((status.error ?? l10n.unknown).toString()),\n        ),\n        icon: const Icon(Icons.error),\n      ),\n    );\n  }\n\n  Widget _buildConnected(SftpReqStatus status) {\n    return _wrapInCard(\n      status: status,\n      subtitle: l10n.sftpSSHConnected,\n      trailing: _buildDelete(status.fileName, status.id),\n    );\n  }\n\n  Widget _buildLoading(SftpReqStatus status) {\n    final percentStr = (status.progress ?? 0.0).toStringAsFixed(2);\n    final size = (status.size ?? 0).bytes2Str;\n    return _wrapInCard(\n      status: status,\n      subtitle: l10n.percentOfSize(percentStr, size),\n      trailing: _buildDelete(status.fileName, status.id),\n    );\n  }\n\n  Widget _buildFinished(SftpReqStatus status) {\n    final time = status.spentTime.toString();\n    final str = l10n.spentTime(time == 'null' ? l10n.unknown : (time.substring(0, time.length - 7)));\n\n    final btns = Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        IconButton(\n          onPressed: () {\n            final idx = status.req.localPath.lastIndexOf(Pfs.seperator);\n            final dir = status.req.localPath.substring(0, idx);\n            LocalFilePage.route.go(context, args: LocalFilePageArgs(initDir: dir));\n          },\n          icon: const Icon(Icons.file_open),\n        ),\n        IconButton(\n          onPressed: () => Pfs.sharePaths(paths: [status.req.localPath]),\n          icon: const Icon(Icons.open_in_new),\n        ),\n      ],\n    );\n\n    return _wrapInCard(status: status, subtitle: str, trailing: btns);\n  }\n\n  Widget _wrapInCard({required SftpReqStatus status, String? subtitle, Widget? trailing}) {\n    final time = DateTime.fromMicrosecondsSinceEpoch(status.id);\n    return CardX(\n      child: ListTile(\n        leading: Text(time.hourMinute),\n        title: Text(status.fileName, overflow: TextOverflow.ellipsis, maxLines: 1),\n        subtitle: subtitle == null ? null : Text(subtitle, style: UIs.textGrey),\n        trailing: trailing,\n      ),\n    );\n  }\n\n  Widget _buildDelete(String name, int id) {\n    return IconButton(\n      onPressed: () => context.showRoundDialog(\n        title: libL10n.attention,\n        child: Text(libL10n.askContinue('${libL10n.delete} ${libL10n.mission}($name)')),\n        actions: Btn.ok(\n          onTap: () {\n            ref.read(sftpProvider.notifier).cancel(id);\n            context.pop();\n          },\n        ).toList,\n      ),\n      icon: const Icon(Icons.delete),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/page/systemd.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/route.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/systemd.dart';\nimport 'package:server_box/data/provider/systemd.dart';\nimport 'package:server_box/view/page/ssh/page/page.dart';\n\nfinal class SystemdPage extends ConsumerStatefulWidget {\n  final SpiRequiredArgs args;\n\n  const SystemdPage({super.key, required this.args});\n\n  static const route = AppRouteArg<void, SpiRequiredArgs>(page: SystemdPage.new, path: '/systemd');\n\n  @override\n  ConsumerState<SystemdPage> createState() => _SystemdPageState();\n}\n\nfinal class _SystemdPageState extends ConsumerState<SystemdPage> {\n  late final _pro = systemdProvider(widget.args.spi);\n\n  late final _notifier = ref.read(_pro.notifier);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: CustomAppBar(\n        title: const Text('Systemd'),\n        actions: isDesktop ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _notifier.getUnits)] : null,\n      ),\n      body: RefreshIndicator(onRefresh: _notifier.getUnits, child: _buildBody()),\n    );\n  }\n\n  Widget _buildBody() {\n    final isBusy = ref.watch(_pro.select((pro) => pro.isBusy));\n    return CustomScrollView(\n      slivers: <Widget>[\n        SliverToBoxAdapter(\n          child: Column(\n            children: [\n              _buildScopeFilterChips(),\n              AnimatedContainer(\n                duration: Durations.medium1,\n                curve: Curves.fastEaseInToSlowEaseOut,\n                height: isBusy ? SizedLoading.medium.size : 0,\n                width: isBusy ? SizedLoading.medium.size : 0,\n                child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),\n              ),\n            ],\n          ),\n        ),\n        _buildUnitList(),\n      ],\n    );\n  }\n\n  Widget _buildScopeFilterChips() {\n    final currentFilter = ref.watch(_pro.select((p) => p.scopeFilter));\n    return Wrap(\n      spacing: 8,\n      children: SystemdScopeFilter.values.map((filter) {\n        final isSelected = filter == currentFilter;\n        return FilterChip(\n          selected: isSelected,\n          label: Text(filter.displayName),\n          onSelected: (_) => _notifier.setScopeFilter(filter),\n        );\n      }).toList(),\n    ).paddingSymmetric(horizontal: 13, vertical: 8);\n  }\n\n  Widget _buildUnitList() {\n    ref.watch(_pro.select((p) => (p.units, p.scopeFilter)));\n    final filteredUnits = _notifier.filteredUnits;\n    if (filteredUnits.isEmpty) {\n      return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));\n    }\n    return SliverList(\n      delegate: SliverChildBuilderDelegate((context, index) {\n        final unit = filteredUnits[index];\n        return ListTile(\n          leading: _buildScopeTag(unit.scope),\n          title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),\n          subtitle: Wrap(\n            children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)],\n          ).paddingOnly(top: 7),\n          trailing: _buildUnitFuncs(unit),\n        ).cardx.paddingSymmetric(horizontal: 13);\n      }, childCount: filteredUnits.length),\n    );\n  }\n\n  Widget _buildUnitFuncs(SystemdUnit unit) {\n    return PopupMenu(\n      items: unit.availableFuncs.map(_buildUnitFuncBtn).toList(),\n      onSelected: (val) => _handleUnitFuncSelected(unit, val),\n    );\n  }\n\n  void _handleUnitFuncSelected(SystemdUnit unit, SystemdUnitFunc func) {\n    final cmd = unit.getCmd(func: func, isRoot: widget.args.spi.isRoot);\n\n    if (func == SystemdUnitFunc.stop || func == SystemdUnitFunc.restart) {\n      _showConfirmDialog(cmd);\n    } else {\n      _navigateToSsh(cmd);\n    }\n  }\n\n  void _showConfirmDialog(String cmd) async {\n    final sure = await context.showRoundDialog(\n      title: libL10n.attention,\n      child: SimpleMarkdown(data: '```shell\\n$cmd\\n```'),\n      actions: [\n        Btn.cancel(),\n        CountDownBtn(\n          seconds: 3,\n          onTap: () => context.pop(true),\n          text: libL10n.ok,\n          afterColor: Colors.red,\n        ),\n      ],\n    );\n    if (sure == true) _navigateToSsh(cmd);\n  }\n\n  void _navigateToSsh(String cmd) {\n    final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd);\n    SSHPage.route.go(context, args);\n  }\n\n  PopupMenuEntry _buildUnitFuncBtn(SystemdUnitFunc func) {\n    return PopupMenuItem<SystemdUnitFunc>(\n      value: func,\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        mainAxisAlignment: MainAxisAlignment.spaceAround,\n        children: [Icon(func.icon, size: 19), const SizedBox(width: 10), Text(func.name.capitalize)],\n      ),\n    );\n  }\n\n  Widget _buildScopeTag(SystemdUnitScope scope) {\n    return _buildTag(scope.name.capitalize, scope.color, true);\n  }\n\n  Widget _buildStateTag(SystemdUnitState state) {\n    return _buildTag(state.name.capitalize, state.color);\n  }\n\n  Widget _buildTypeTag(SystemdUnitType type) {\n    return _buildTag(type.name.capitalize);\n  }\n\n  Widget _buildTag(String tag, [Color? color, bool noPad = false]) {\n    return Container(\n      decoration: BoxDecoration(\n        color: color?.withValues(alpha: 0.7) ?? UIs.halfAlpha,\n        borderRadius: BorderRadius.circular(5),\n      ),\n      child: Text(tag, style: UIs.text11).paddingSymmetric(horizontal: 5, vertical: 1),\n    ).paddingOnly(right: noPad ? 0 : 5);\n  }\n}\n"
  },
  {
    "path": "lib/view/widget/omit_start_text.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass OmitStartText extends StatelessWidget {\n  final String text;\n  final int? maxLines;\n  final TextStyle? style;\n  final TextOverflow? overflow;\n\n  const OmitStartText(this.text, {super.key, this.maxLines, this.style, this.overflow});\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (context, size) {\n        bool exceeded = false;\n        int len = 0;\n        for (; !exceeded && len < text.length; len++) {\n          // Build the textspan\n          final span = TextSpan(\n            text: 'A' * 7 + text.substring(text.length - len),\n            style: style ?? Theme.of(context).textTheme.bodyMedium,\n          );\n\n          // Use a textpainter to determine if it will exceed max lines\n          final tp = TextPainter(maxLines: maxLines ?? 1, textDirection: TextDirection.ltr, text: span);\n\n          // trigger it to layout\n          tp.layout(maxWidth: size.maxWidth);\n\n          // whether the text overflowed or not\n          exceeded = tp.didExceedMaxLines;\n        }\n\n        return Text(\n          (exceeded ? '...' : '') + text.substring(text.length - len),\n          overflow: overflow ?? TextOverflow.fade,\n          softWrap: false,\n          maxLines: maxLines ?? 1,\n          style: style,\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/widget/percent_circle.dart",
    "content": "import 'package:circle_chart/circle_chart.dart';\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\n\nfinal class PercentCircle extends StatelessWidget {\n  final double percent;\n\n  const PercentCircle({super.key, required this.percent});\n\n  @override\n  Widget build(BuildContext context) {\n    final percent = switch (this.percent) {\n      <= 0.01 => 0.01,\n      >= 99.9 => 99.9,\n      _ => this.percent,\n    };\n    return Stack(\n      alignment: Alignment.center,\n      children: [\n        CircleChart(\n          progressColor: UIs.primaryColor,\n          progressNumber: percent,\n          maxNumber: 100,\n          width: 57,\n          height: 57,\n          animationDuration: const Duration(milliseconds: 777),\n        ),\n        Text(\n          '${percent.toStringAsFixed(1)}%',\n          textAlign: TextAlign.center,\n          style: const TextStyle(fontSize: 12.7),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/view/widget/server_func_btns.dart",
    "content": "import 'dart:io';\n\nimport 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:server_box/core/extension/context/locale.dart';\nimport 'package:server_box/core/route.dart';\nimport 'package:server_box/core/utils/server.dart';\nimport 'package:server_box/data/model/app/menu/base.dart';\nimport 'package:server_box/data/model/app/menu/server_func.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\nimport 'package:server_box/data/model/server/snippet.dart';\nimport 'package:server_box/data/provider/server/single.dart';\nimport 'package:server_box/data/provider/snippet.dart';\nimport 'package:server_box/data/res/store.dart';\nimport 'package:server_box/view/page/container/container.dart';\nimport 'package:server_box/view/page/iperf.dart';\nimport 'package:server_box/view/page/process.dart';\nimport 'package:server_box/view/page/ssh/page/page.dart';\nimport 'package:server_box/view/page/storage/sftp.dart';\nimport 'package:server_box/view/page/systemd.dart';\n\nclass ServerFuncBtnsTopRight extends ConsumerWidget {\n  final Spi spi;\n\n  const ServerFuncBtnsTopRight({super.key, required this.spi});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    return PopupMenu<ServerFuncBtn>(\n      items: ServerFuncBtn.values.map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),\n      padding: const EdgeInsets.symmetric(horizontal: 10),\n      onSelected: (val) => _onTapMoreBtns(val, spi, context, ref),\n    );\n  }\n}\n\nclass ServerFuncBtns extends StatelessWidget {\n  const ServerFuncBtns({super.key, required this.spi});\n\n  final Spi spi;\n\n  @override\n  Widget build(BuildContext context) {\n    final btns = this.btns;\n    if (btns.isEmpty) return UIs.placeholder;\n\n    return SizedBox(\n      height: 77,\n      child: ListView.builder(\n        itemCount: btns.length,\n        scrollDirection: Axis.horizontal,\n        padding: EdgeInsets.symmetric(horizontal: 13),\n        itemBuilder: (context, index) {\n          final value = btns[index];\n          final item = Consumer(builder: (_, ref, _) => _buildItem(context, value, ref));\n          return item.paddingSymmetric(horizontal: 7);\n        },\n      ),\n    );\n  }\n\n  Widget _buildItem(BuildContext context, ServerFuncBtn e, WidgetRef ref) {\n    final move = Stores.setting.moveServerFuncs.fetch();\n    if (move) {\n      return IconButton(\n        onPressed: () => _onTapMoreBtns(e, spi, context, ref),\n        padding: EdgeInsets.zero,\n        tooltip: e.toStr,\n        icon: Icon(e.icon, size: 15),\n      );\n    }\n\n    return Padding(\n      padding: const EdgeInsets.only(bottom: 13),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          IconButton(\n            onPressed: () => _onTapMoreBtns(e, spi, context, ref),\n            padding: EdgeInsets.zero,\n            icon: Icon(e.icon, size: 17),\n          ),\n          Text(e.toStr, style: UIs.text11Grey),\n        ],\n      ),\n    );\n  }\n\n  List<ServerFuncBtn> get btns {\n    try {\n      final vals = <ServerFuncBtn>[];\n      final list = Stores.setting.serverFuncBtns.fetch();\n      for (final idx in list) {\n        if (idx < 0 || idx >= ServerFuncBtn.values.length) continue;\n        vals.add(ServerFuncBtn.values[idx]);\n      }\n      return vals;\n    } catch (e) {\n      return ServerFuncBtn.values;\n    }\n  }\n}\n\nvoid _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context, WidgetRef ref) async {\n  // final isMobile = ResponsiveBreakpoints.of(context).isMobile;\n  switch (value) {\n    // case ServerFuncBtn.pkg:\n    //   _onPkg(context, spi);\n    //   break;\n    case ServerFuncBtn.sftp:\n      if (!_checkClient(context, spi.id, ref)) return;\n      final args = SftpPageArgs(spi: spi);\n      // if (isMobile) {\n      SftpPage.route.go(context, args);\n      // } else {\n      //   SplitViewNavigator.of(context)?.replace(\n      //     SftpPage.route.toWidget(args: args),\n      //   );\n      // }\n\n      break;\n    case ServerFuncBtn.snippet:\n      final snippetState = ref.read(snippetProvider);\n      if (snippetState.snippets.isEmpty) {\n        context.showSnackBar(libL10n.empty);\n        return;\n      }\n      final snippets = await context.showPickWithTagDialog<Snippet>(\n        title: libL10n.snippet,\n        tags: snippetState.tags.vn,\n        itemsBuilder: (e) {\n          if (e == TagSwitcher.kDefaultTag) {\n            return snippetState.snippets;\n          }\n          return snippetState.snippets\n              .where((element) => element.tags?.contains(e) ?? false)\n              .toList();\n        },\n        display: (e) => e.name,\n      );\n      if (snippets == null || snippets.isEmpty) return;\n      final snippet = snippets.firstOrNull;\n      if (snippet == null) return;\n      final fmted = snippet.fmtWithSpi(spi);\n      final sure = await context.showRoundDialog<bool>(\n        title: libL10n.attention,\n        child: SingleChildScrollView(child: SimpleMarkdown(data: '```shell\\n$fmted\\n```')),\n        actions: [CountDownBtn(onTap: () => context.pop(true), text: libL10n.run, afterColor: Colors.red)],\n      );\n      if (sure != true) return;\n      if (!_checkClient(context, spi.id, ref)) return;\n      final args = SshPageArgs(spi: spi, initSnippet: snippet);\n      // if (isMobile) {\n      SSHPage.route.go(context, args);\n      // } else {\n      //   SplitViewNavigator.of(context)?.replace(\n      //     SSHPage.route.toWidget(args: args),\n      //   );\n      // }\n      break;\n    case ServerFuncBtn.container:\n      if (!_checkClient(context, spi.id, ref)) return;\n      final args = SpiRequiredArgs(spi);\n      // if (isMobile) {\n      ContainerPage.route.go(context, args);\n      // } else {\n      //   SplitViewNavigator.of(\n      //     context,\n      //   )?.replace(ContainerPage.route.toWidget(args: args));\n      // }\n      break;\n    case ServerFuncBtn.process:\n      if (!_checkClient(context, spi.id, ref)) return;\n      final args = SpiRequiredArgs(spi);\n      // if (isMobile) {\n      ProcessPage.route.go(context, args);\n      // } else {\n      //   SplitViewNavigator.of(context)?.replace(\n      //     ProcessPage.route.toWidget(args: args),\n      //   );\n      // }\n      break;\n    case ServerFuncBtn.terminal:\n      _gotoSSH(spi, context);\n      break;\n    case ServerFuncBtn.iperf:\n      if (!_checkClient(context, spi.id, ref)) return;\n      final args = SpiRequiredArgs(spi);\n      // if (isMobile) {\n      IPerfPage.route.go(context, args);\n      // } else {\n      //   SplitViewNavigator.of(context)?.replace(\n      //     IPerfPage.route.toWidget(args: args),\n      //   );\n      // }\n      break;\n    case ServerFuncBtn.systemd:\n      if (!_checkClient(context, spi.id, ref)) return;\n      final args = SpiRequiredArgs(spi);\n      // if (isMobile) {\n      SystemdPage.route.go(context, args);\n      // } else {\n      //   SplitViewNavigator.of(context)?.replace(\n      //     SystemdPage.route.toWidget(args: args),\n      //   );\n      // }\n      break;\n  }\n}\n\nvoid _gotoSSH(Spi spi, BuildContext context) async {\n  // run built-in ssh on macOS due to incompatibility\n  if (isMobile || isMacOS) {\n    final args = SshPageArgs(spi: spi);\n    SSHPage.route.go(context, args);\n    return;\n  }\n\n  final extraArgs = <String>[];\n  if (spi.port != 22) {\n    extraArgs.addAll(['-p', '${spi.port}']);\n  }\n\n  final path = await () async {\n    final tempKeyFileName = 'srvbox_pk_${spi.keyId}';\n\n    /// For security reason, save the private key file to app doc path\n    return Paths.doc.joinPath(tempKeyFileName);\n  }();\n\n  final file = File(path);\n  final shouldGenKey = spi.keyId != null;\n  if (shouldGenKey) {\n    if (await file.exists()) {\n      await file.delete();\n    }\n    final keyContent = getPrivateKey(spi.keyId!);\n    final keyContentWithNewline = keyContent.endsWith('\\n') ? keyContent : '$keyContent\\n';\n    await file.writeAsString(keyContentWithNewline);\n    if (!Platform.isWindows) {\n      await Process.run('chmod', ['600', path]);\n    }\n    extraArgs.addAll(['-i', path]);\n  }\n\n  final sshCommand = ['ssh'] + extraArgs + ['${spi.user}@${spi.ip}'];\n\n  final system = Pfs.type;\n  switch (system) {\n    case Pfs.windows:\n      await Process.start('cmd', ['/c', 'start'] + sshCommand);\n      break;\n    case Pfs.linux:\n      final scriptFile = File('${Directory.systemTemp.path}/srvbox_launch_term.sh');\n      await scriptFile.writeAsString(_runEmulatorShell);\n\n      if (Platform.isLinux || Platform.isMacOS) {\n        await Process.run('chmod', ['+x', scriptFile.path]);\n      }\n\n      try {\n        var terminal = Stores.setting.desktopTerminal.fetch();\n        if (terminal.isEmpty) terminal = 'x-terminal-emulator';\n\n        await Process.start(scriptFile.path, [terminal, ...sshCommand]);\n      } catch (e, s) {\n        context.showErrDialog(e, s, libL10n.emulator);\n      } finally {\n        await scriptFile.delete();\n      }\n      break;\n    default:\n      context.showSnackBar('Mismatch system: $system');\n  }\n\n  if (shouldGenKey) {\n    if (!await file.exists()) return;\n    await Future.delayed(const Duration(seconds: 2), file.delete);\n  }\n}\n\nbool _checkClient(BuildContext context, String id, WidgetRef ref) {\n  final serverState = ref.read(serverProvider(id));\n  if (serverState.client == null) {\n    context.showSnackBar(l10n.waitConnection);\n    return false;\n  }\n  return true;\n}\n\nconst _runEmulatorShell = '''\n#!/bin/sh\nTERMINAL=\"\\$1\"\nshift\n\nif [ -z \"\\$TERMINAL\" ] || [ \"\\$TERMINAL\" = \"x-terminal-emulator\" ]; then\n    for term in kitty alacritty gnome-terminal gnome-console konsole xfce4-terminal terminator tilix wezterm foot xterm; do\n        command -v \"\\$term\" >/dev/null 2>&1 && { TERMINAL=\"\\$term\"; break; }\n    done\n    [ -z \"\\$TERMINAL\" ] && TERMINAL=\"x-terminal-emulator\"\nfi\n\ncase \"\\$TERMINAL\" in\n    gnome-terminal|gnome-console) exec \"\\$TERMINAL\" -- \"\\$@\" ;;\n    alacritty) \n        \"\\$TERMINAL\" --version 2>&1 | grep -q \"alacritty 0\\\\.1[3-9]\" && \n        exec \"\\$TERMINAL\" --command \"\\$@\" || exec \"\\$TERMINAL\" -e \"\\$@\" ;;\n    kitty|foot) exec \"\\$TERMINAL\" \"\\$@\" ;;\n    wezterm) exec \"\\$TERMINAL\" start -- \"\\$@\" ;;\n    xfce4-terminal) exec \"\\$TERMINAL\" -e \"\\$*\" ;;\n    *) exec \"\\$TERMINAL\" -e \"\\$@\" ;;\nesac\n''';\n"
  },
  {
    "path": "lib/view/widget/unix_perm.dart",
    "content": "import 'package:fl_lib/fl_lib.dart';\nimport 'package:flutter/material.dart';\n\nfinal class UnixPermOp {\n  final bool r;\n  final bool w;\n  final bool x;\n\n  const UnixPermOp({required this.r, required this.w, required this.x});\n\n  UnixPermOp copyWith({bool? r, bool? w, bool? x}) {\n    return UnixPermOp(r: r ?? this.r, w: w ?? this.w, x: x ?? this.x);\n  }\n\n  int get value {\n    return (r ? 4 : 0) + (w ? 2 : 0) + (x ? 1 : 0);\n  }\n}\n\nenum UnixPermScope {\n  user,\n  group,\n  other;\n\n  String get title {\n    return switch (this) {\n      user => 'User',\n      group => 'Group',\n      other => 'Other',\n    };\n  }\n}\n\nfinal class UnixPerm {\n  final UnixPermOp user;\n  final UnixPermOp group;\n  final UnixPermOp other;\n\n  const UnixPerm({\n    required this.user,\n    required this.group,\n    required this.other,\n  });\n\n  UnixPerm copyWith({UnixPermOp? user, UnixPermOp? group, UnixPermOp? other}) {\n    return UnixPerm(\n      user: user ?? this.user,\n      group: group ?? this.group,\n      other: other ?? this.other,\n    );\n  }\n\n  UnixPerm copyWithScope(UnixPermScope scope, UnixPermOp rwx) {\n    switch (scope) {\n      case UnixPermScope.user:\n        return copyWith(user: rwx);\n      case UnixPermScope.group:\n        return copyWith(group: rwx);\n      case UnixPermScope.other:\n        return copyWith(other: rwx);\n    }\n  }\n\n  /// eg.: 744\n  String get perm {\n    return '${user.value}${group.value}${other.value}';\n  }\n\n  static UnixPerm get empty => const UnixPerm(\n    user: UnixPermOp(r: false, w: false, x: false),\n    group: UnixPermOp(r: false, w: false, x: false),\n    other: UnixPermOp(r: false, w: false, x: false),\n  );\n}\n\nfinal class UnixPermEditor extends StatefulWidget {\n  final UnixPerm perm;\n  final void Function(UnixPerm) onChanged;\n\n  const UnixPermEditor({\n    super.key,\n    required this.perm,\n    required this.onChanged,\n  });\n\n  @override\n  State<UnixPermEditor> createState() => _UnixPermEditorState();\n}\n\nfinal class _UnixPermEditorState extends State<UnixPermEditor> {\n  late UnixPerm perm;\n\n  @override\n  void initState() {\n    super.initState();\n    perm = widget.perm;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        const Row(\n          mainAxisAlignment: MainAxisAlignment.spaceAround,\n          children: [\n            Text('Read'),\n            Text('Writ'), // Keep it short to fit UI\n            Text('Exec'),\n          ],\n        ).paddingOnly(left: 13),\n        UIs.height7,\n        _buildRow(UnixPermScope.user, perm.user),\n        _buildRow(UnixPermScope.group, perm.group),\n        _buildRow(UnixPermScope.other, perm.other),\n      ],\n    );\n  }\n\n  Widget _buildRow(UnixPermScope scope, UnixPermOp rwx) {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.spaceBetween,\n      children: [\n        SizedBox(width: 7, child: Text(scope.title)),\n        _buildSwitch(rwx.r, (v) {\n          setState(() {\n            perm = perm.copyWithScope(scope, rwx.copyWith(r: v));\n            widget.onChanged(perm);\n          });\n        }),\n        _buildSwitch(rwx.w, (v) {\n          setState(() {\n            perm = perm.copyWithScope(scope, rwx.copyWith(w: v));\n            widget.onChanged(perm);\n          });\n        }),\n        _buildSwitch(rwx.x, (v) {\n          setState(() {\n            perm = perm.copyWithScope(scope, rwx.copyWith(x: v));\n            widget.onChanged(perm);\n          });\n        }),\n      ],\n    );\n  }\n\n  Widget _buildSwitch(bool value, void Function(bool) onChanged) {\n    return Switch(value: value, onChanged: onChanged);\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 \"ServerBox\")\n# The unique GTK application identifier for this application. See:\n# https://wiki.gnome.org/HowDoI/ChooseApplicationID\nset(APPLICATION_ID \"tech.lolli.toolbox\")\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\")\n# if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n#   set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\n# endif()\n# Always set the install prefix to the build bundle directory, even if\n# CMAKE_INSTALL_PREFIX was set to something else before.\nset(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\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# Copy the native assets provided by the build.dart from all packages.\nset(NATIVE_ASSETS_DIR \"${PROJECT_BUILD_DIR}native_assets/linux/\")\ninstall(DIRECTORY \"${NATIVE_ASSETS_DIR}\"\n   DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n   COMPONENT Runtime)\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/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 <dynamic_color/dynamic_color_plugin.h>\n#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>\n#include <gtk/gtk_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\nvoid fl_register_plugins(FlPluginRegistry* registry) {\n  g_autoptr(FlPluginRegistrar) dynamic_color_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"DynamicColorPlugin\");\n  dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);\n  g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"FlutterSecureStorageLinuxPlugin\");\n  flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);\n  g_autoptr(FlPluginRegistrar) gtk_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"GtkPlugin\");\n  gtk_plugin_register_with_registrar(gtk_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}\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  dynamic_color\n  flutter_secure_storage_linux\n  gtk\n  screen_retriever_linux\n  url_launcher_linux\n  window_manager\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, \"ServerBox\");\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, \"ServerBox\");\n  }\n\n  gtk_window_set_default_size(window, 400, 777);\n  gtk_widget_realize(GTK_WIDGET(window));\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 GApplication::startup.\nstatic void my_application_startup(GApplication* application) {\n  //MyApplication* self = MY_APPLICATION(object);\n\n  // Perform any actions required at application startup.\n\n  G_APPLICATION_CLASS(my_application_parent_class)->startup(application);\n}\n\n// Implements GApplication::shutdown.\nstatic void my_application_shutdown(GApplication* application) {\n  //MyApplication* self = MY_APPLICATION(object);\n\n  // Perform any actions required at application shutdown.\n\n  G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);\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_APPLICATION_CLASS(klass)->startup = my_application_startup;\n  G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;\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/packaging/appimage/make_config.yaml",
    "content": "display_name: ServerBox\npackage_name: cn.lpkt.serverbox\nmaintainer:\n  name: lollipopkit\n  email: 10864310+lollipopkit@users.noreply.github.com\n# co_authors:\n#   - name: \n#     email: \npriority: optional\nsection: x11\ninstalled_size: 6604\nessential: false\nicon: assets/app_icon.png\n\n# postinstall_scripts:\n#   - echo \"Installed my awesome app\"\n# postuninstall_scripts:\n#   - echo \"Surprised Pickachu face\"\n\nkeywords:\n  - server\n  - ssh\n  - sftp\n  - system\n\ngeneric_name: ServerBox\n\ncategories:\n  - Utility\n\nstartup_notify: true"
  },
  {
    "path": "linux/packaging/deb/make_config.yaml",
    "content": "display_name: ServerBox\npackage_name: cn.lpkt.serverbox\nmaintainer:\n  name: lollipopkit\n  email: 10864310+lollipopkit@users.noreply.github.com\n# co_authors:\n#   - name: \n#     email: \npriority: optional\nsection: x11\ninstalled_size: 6604\nessential: false\nicon: assets/app_icon.png\n\n# postinstall_scripts:\n#   - echo \"Installed my awesome app\"\n# postuninstall_scripts:\n#   - echo \"Surprised Pickachu face\"\n\nkeywords:\n  - server\n  - ssh\n  - sftp\n  - system\n\ngeneric_name: ServerBox\n\ncategories:\n  - Utility\n\nstartup_notify: true"
  },
  {
    "path": "linux/packaging/rpm/make_config.yaml",
    "content": "icon: assets/app_icon.png\nsummary: Server status and tools to manage server.\ngroup: Application/Utility\nvendor: lollipopkit\npackager: lollipopkit\npackagerEmail: 10864310+lollipopkit@users.noreply.github.com\nlicense: GPLv3\nurl: https://github.com/lollipopkit/flutter_server_box\n\ndisplay_name: ServerBox\n\nkeywords:\n  - server\n  - ssh\n  - sftp\n  - system\n\ngeneric_name: ServerBox\n\ncategories:\n  - Utility\n\nstartup_notify: true"
  },
  {
    "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 app_links\nimport dynamic_color\nimport file_picker\nimport flutter_secure_storage_macos\nimport icloud_storage\nimport local_auth_darwin\nimport package_info_plus\nimport screen_retriever_macos\nimport share_plus\nimport shared_preferences_foundation\nimport url_launcher_macos\nimport wakelock_plus\nimport window_manager\n\nfunc RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {\n  AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: \"AppLinksMacosPlugin\"))\n  DynamicColorPlugin.register(with: registry.registrar(forPlugin: \"DynamicColorPlugin\"))\n  FilePickerPlugin.register(with: registry.registrar(forPlugin: \"FilePickerPlugin\"))\n  FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: \"FlutterSecureStoragePlugin\"))\n  IcloudStoragePlugin.register(with: registry.registrar(forPlugin: \"IcloudStoragePlugin\"))\n  LocalAuthPlugin.register(with: registry.registrar(forPlugin: \"LocalAuthPlugin\"))\n  FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: \"FPPPackageInfoPlusPlugin\"))\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  WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: \"WakelockPlusMacosPlugin\"))\n  WindowManagerPlugin.register(with: registry.registrar(forPlugin: \"WindowManagerPlugin\"))\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  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n    return true\n  }\n\n  override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {\n    return true\n  }\n\n  override func applicationDidFinishLaunching(_ notification: Notification) {\n    if let controller = mainFlutterWindow?.contentViewController as? FlutterViewController {\n      let channel = FlutterMethodChannel(name: \"about\", binaryMessenger: controller.engine.binaryMessenger)\n      channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in\n        if call.method == \"showAboutPanel\" {\n          NSApp.orderFrontStandardAboutPanel(nil)\n          result(nil)\n        } else {\n          result(FlutterMethodNotImplemented)\n        }\n      }\n    }\n    super.applicationDidFinishLaunching(notification)\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=\"14490.70\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"14490.70\"/>\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=\"Runner\" 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                            <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                            <menuItem title=\"Quit APP_NAME\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                <connections>\n                                    <action selector=\"terminate:\" target=\"-1\" id=\"Te7-pn-YzF\"/>\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=\"Runner\" 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=\"2560\" height=\"1577\"/>\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        </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 copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2023 tech.lolli. 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.developer.icloud-container-identifiers</key>\n\t<array>\n\t\t<string>iCloud.tech.lolli.serverbox</string>\n\t</array>\n\t<key>com.apple.developer.icloud-services</key>\n\t<array>\n\t\t<string>CloudDocuments</string>\n\t</array>\n\t<key>com.apple.developer.ubiquity-container-identifiers</key>\n\t<array>\n\t\t<string>iCloud.tech.lolli.serverbox</string>\n\t</array>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.files.user-selected.read-write</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>keychain-access-groups</key>\n\t<array/>\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>ITSAppUsesNonExemptEncryption</key>\n\t<false/>\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>LSApplicationCategoryType</key>\n\t<string>public.app-category.utilities</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\t<key>NSFileProviderExtensionAccessControlPolicy</key>\n\t<string>NSFileProviderExtensionAccessControlPolicyRequiresExplicitAccess</string>\n\t<key>NSFileProviderExtensionUsageDescription</key>\n\t<string>This app needs to access SSH configuration files to import server connection settings from ~/.ssh/config. This allows users to quickly add their existing SSH servers without manual configuration.</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\nimport window_manager\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController()\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n    let newWindowRect = NSRect(\n      x: windowFrame.origin.x, \n      y: windowFrame.origin.y, \n      width: 400, \n      height: 777\n    )\n    self.setFrame(newWindowRect, display: true)\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n\n  override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {\n    super.order(place, relativeTo: otherWin)\n    hiddenWindowAtLaunch()\n  }\n}\n"
  },
  {
    "path": "macos/Runner/PrivacyInfo.xcprivacy",
    "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<!--\n   PrivacyInfo.xcprivacy\n   Runner\n\n   Created by lollipopkit on 31/3/2024.\n   Copyright (c) 2024 . All rights reserved.\n-->\n<plist version=\"1.0\">\n<dict>\n\t<key>NSPrivacyAccessedAPITypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>NSPrivacyAccessedAPITypeReasons</key>\n\t\t\t<array>\n\t\t\t\t<string>E174.1</string>\n\t\t\t</array>\n\t\t\t<key>NSPrivacyAccessedAPIType</key>\n\t\t\t<string>NSPrivacyAccessedAPICategoryDiskSpace</string>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\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.developer.icloud-container-identifiers</key>\n\t<array>\n\t\t<string>iCloud.tech.lolli.serverbox</string>\n\t</array>\n\t<key>com.apple.developer.icloud-services</key>\n\t<array>\n\t\t<string>CloudDocuments</string>\n\t</array>\n\t<key>com.apple.developer.ubiquity-container-identifiers</key>\n\t<array>\n\t\t<string>iCloud.tech.lolli.serverbox</string>\n\t</array>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.files.user-selected.read-write</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>keychain-access-groups</key>\n\t<array/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/de.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenu\"; title = \"Find\"; ObjectID = \"1b7-l0-nxx\"; */\n\"1b7-l0-nxx.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Spelling\"; ObjectID = \"3IN-sU-3Bg\"; */\n\"3IN-sU-3Bg.title\" = \"Spelling\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Find\"; ObjectID = \"4EN-yA-p0u\"; */\n\"4EN-yA-p0u.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenuItem\"; title = \"Correct Spelling Automatically\"; ObjectID = \"78Y-hA-62v\"; */\n\"78Y-hA-62v.title\" = \"Correct Spelling Automatically\";\n\n/* Class = \"NSMenuItem\"; title = \"Substitutions\"; ObjectID = \"9ic-FL-obx\"; */\n\"9ic-FL-obx.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Copy/Paste\"; ObjectID = \"9yt-4B-nSM\"; */\n\"9yt-4B-nSM.title\" = \"Smart Copy/Paste\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Preferences…\"; ObjectID = \"BOF-NM-1cW\"; */\n\"BOF-NM-1cW.title\" = \"Preferences…\";\n\n/* Class = \"NSMenuItem\"; title = \"Spelling and Grammar\"; ObjectID = \"Dv1-io-Yv7\"; */\n\"Dv1-io-Yv7.title\" = \"Spelling and Grammar\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"Substitutions\"; ObjectID = \"FeM-D8-WVr\"; */\n\"FeM-D8-WVr.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Text Replacement\"; ObjectID = \"HFQ-gK-NFA\"; */\n\"HFQ-gK-NFA.title\" = \"Text Replacement\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Spelling and Grammar\"; ObjectID = \"HFo-cy-zxI\"; */\n\"HFo-cy-zxI.title\" = \"Show Spelling and Grammar\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Previous\"; ObjectID = \"OwM-mh-QMV\"; */\n\"OwM-mh-QMV.title\" = \"Find Previous\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenuItem\"; title = \"Jump to Selection\"; ObjectID = \"S0p-oC-mLd\"; */\n\"S0p-oC-mLd.title\" = \"Jump to Selection\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste and Match Style\"; ObjectID = \"WeT-3V-zwk\"; */\n\"WeT-3V-zwk.title\" = \"Paste and Match Style\";\n\n/* Class = \"NSMenuItem\"; title = \"Find…\"; ObjectID = \"Xz5-n4-O0W\"; */\n\"Xz5-n4-O0W.title\" = \"Find…\";\n\n/* Class = \"NSMenuItem\"; title = \"Find and Replace…\"; ObjectID = \"YEy-JH-Tfz\"; */\n\"YEy-JH-Tfz.title\" = \"Find and Replace…\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Use Selection for Find\"; ObjectID = \"buJ-ug-pKt\"; */\n\"buJ-ug-pKt.title\" = \"Use Selection for Find\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Links\"; ObjectID = \"cwL-P1-jid\"; */\n\"cwL-P1-jid.title\" = \"Smart Links\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Quotes\"; ObjectID = \"hQb-2v-fYv\"; */\n\"hQb-2v-fYv.title\" = \"Smart Quotes\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Document Now\"; ObjectID = \"hz2-CU-CR7\"; */\n\"hz2-CU-CR7.title\" = \"Check Document Now\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Grammar With Spelling\"; ObjectID = \"mK6-2p-4JG\"; */\n\"mK6-2p-4JG.title\" = \"Check Grammar With Spelling\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Next\"; ObjectID = \"q09-fT-Sye\"; */\n\"q09-fT-Sye.title\" = \"Find Next\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Spelling While Typing\"; ObjectID = \"rbD-Rh-wIN\"; */\n\"rbD-Rh-wIN.title\" = \"Check Spelling While Typing\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Dashes\"; ObjectID = \"rgM-f4-ycn\"; */\n\"rgM-f4-ycn.title\" = \"Smart Dashes\";\n\n/* Class = \"NSMenuItem\"; title = \"Data Detectors\"; ObjectID = \"tRr-pd-1PS\"; */\n\"tRr-pd-1PS.title\" = \"Data Detectors\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Substitutions\"; ObjectID = \"z6F-FW-3nz\"; */\n\"z6F-FW-3nz.title\" = \"Show Substitutions\";\n"
  },
  {
    "path": "macos/Runner/es.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenu\"; title = \"Find\"; ObjectID = \"1b7-l0-nxx\"; */\n\"1b7-l0-nxx.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Spelling\"; ObjectID = \"3IN-sU-3Bg\"; */\n\"3IN-sU-3Bg.title\" = \"Spelling\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Find\"; ObjectID = \"4EN-yA-p0u\"; */\n\"4EN-yA-p0u.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenuItem\"; title = \"Correct Spelling Automatically\"; ObjectID = \"78Y-hA-62v\"; */\n\"78Y-hA-62v.title\" = \"Correct Spelling Automatically\";\n\n/* Class = \"NSMenuItem\"; title = \"Substitutions\"; ObjectID = \"9ic-FL-obx\"; */\n\"9ic-FL-obx.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Copy/Paste\"; ObjectID = \"9yt-4B-nSM\"; */\n\"9yt-4B-nSM.title\" = \"Smart Copy/Paste\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Preferences…\"; ObjectID = \"BOF-NM-1cW\"; */\n\"BOF-NM-1cW.title\" = \"Preferences…\";\n\n/* Class = \"NSMenuItem\"; title = \"Spelling and Grammar\"; ObjectID = \"Dv1-io-Yv7\"; */\n\"Dv1-io-Yv7.title\" = \"Spelling and Grammar\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"Substitutions\"; ObjectID = \"FeM-D8-WVr\"; */\n\"FeM-D8-WVr.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Text Replacement\"; ObjectID = \"HFQ-gK-NFA\"; */\n\"HFQ-gK-NFA.title\" = \"Text Replacement\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Spelling and Grammar\"; ObjectID = \"HFo-cy-zxI\"; */\n\"HFo-cy-zxI.title\" = \"Show Spelling and Grammar\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Previous\"; ObjectID = \"OwM-mh-QMV\"; */\n\"OwM-mh-QMV.title\" = \"Find Previous\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenuItem\"; title = \"Jump to Selection\"; ObjectID = \"S0p-oC-mLd\"; */\n\"S0p-oC-mLd.title\" = \"Jump to Selection\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste and Match Style\"; ObjectID = \"WeT-3V-zwk\"; */\n\"WeT-3V-zwk.title\" = \"Paste and Match Style\";\n\n/* Class = \"NSMenuItem\"; title = \"Find…\"; ObjectID = \"Xz5-n4-O0W\"; */\n\"Xz5-n4-O0W.title\" = \"Find…\";\n\n/* Class = \"NSMenuItem\"; title = \"Find and Replace…\"; ObjectID = \"YEy-JH-Tfz\"; */\n\"YEy-JH-Tfz.title\" = \"Find and Replace…\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Use Selection for Find\"; ObjectID = \"buJ-ug-pKt\"; */\n\"buJ-ug-pKt.title\" = \"Use Selection for Find\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Links\"; ObjectID = \"cwL-P1-jid\"; */\n\"cwL-P1-jid.title\" = \"Smart Links\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Quotes\"; ObjectID = \"hQb-2v-fYv\"; */\n\"hQb-2v-fYv.title\" = \"Smart Quotes\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Document Now\"; ObjectID = \"hz2-CU-CR7\"; */\n\"hz2-CU-CR7.title\" = \"Check Document Now\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Grammar With Spelling\"; ObjectID = \"mK6-2p-4JG\"; */\n\"mK6-2p-4JG.title\" = \"Check Grammar With Spelling\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Next\"; ObjectID = \"q09-fT-Sye\"; */\n\"q09-fT-Sye.title\" = \"Find Next\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Spelling While Typing\"; ObjectID = \"rbD-Rh-wIN\"; */\n\"rbD-Rh-wIN.title\" = \"Check Spelling While Typing\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Dashes\"; ObjectID = \"rgM-f4-ycn\"; */\n\"rgM-f4-ycn.title\" = \"Smart Dashes\";\n\n/* Class = \"NSMenuItem\"; title = \"Data Detectors\"; ObjectID = \"tRr-pd-1PS\"; */\n\"tRr-pd-1PS.title\" = \"Data Detectors\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Substitutions\"; ObjectID = \"z6F-FW-3nz\"; */\n\"z6F-FW-3nz.title\" = \"Show Substitutions\";\n"
  },
  {
    "path": "macos/Runner/fr.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenu\"; title = \"Find\"; ObjectID = \"1b7-l0-nxx\"; */\n\"1b7-l0-nxx.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Spelling\"; ObjectID = \"3IN-sU-3Bg\"; */\n\"3IN-sU-3Bg.title\" = \"Spelling\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Find\"; ObjectID = \"4EN-yA-p0u\"; */\n\"4EN-yA-p0u.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenuItem\"; title = \"Correct Spelling Automatically\"; ObjectID = \"78Y-hA-62v\"; */\n\"78Y-hA-62v.title\" = \"Correct Spelling Automatically\";\n\n/* Class = \"NSMenuItem\"; title = \"Substitutions\"; ObjectID = \"9ic-FL-obx\"; */\n\"9ic-FL-obx.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Copy/Paste\"; ObjectID = \"9yt-4B-nSM\"; */\n\"9yt-4B-nSM.title\" = \"Smart Copy/Paste\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Preferences…\"; ObjectID = \"BOF-NM-1cW\"; */\n\"BOF-NM-1cW.title\" = \"Preferences…\";\n\n/* Class = \"NSMenuItem\"; title = \"Spelling and Grammar\"; ObjectID = \"Dv1-io-Yv7\"; */\n\"Dv1-io-Yv7.title\" = \"Spelling and Grammar\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"Substitutions\"; ObjectID = \"FeM-D8-WVr\"; */\n\"FeM-D8-WVr.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Text Replacement\"; ObjectID = \"HFQ-gK-NFA\"; */\n\"HFQ-gK-NFA.title\" = \"Text Replacement\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Spelling and Grammar\"; ObjectID = \"HFo-cy-zxI\"; */\n\"HFo-cy-zxI.title\" = \"Show Spelling and Grammar\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Previous\"; ObjectID = \"OwM-mh-QMV\"; */\n\"OwM-mh-QMV.title\" = \"Find Previous\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenuItem\"; title = \"Jump to Selection\"; ObjectID = \"S0p-oC-mLd\"; */\n\"S0p-oC-mLd.title\" = \"Jump to Selection\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste and Match Style\"; ObjectID = \"WeT-3V-zwk\"; */\n\"WeT-3V-zwk.title\" = \"Paste and Match Style\";\n\n/* Class = \"NSMenuItem\"; title = \"Find…\"; ObjectID = \"Xz5-n4-O0W\"; */\n\"Xz5-n4-O0W.title\" = \"Find…\";\n\n/* Class = \"NSMenuItem\"; title = \"Find and Replace…\"; ObjectID = \"YEy-JH-Tfz\"; */\n\"YEy-JH-Tfz.title\" = \"Find and Replace…\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Use Selection for Find\"; ObjectID = \"buJ-ug-pKt\"; */\n\"buJ-ug-pKt.title\" = \"Use Selection for Find\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Links\"; ObjectID = \"cwL-P1-jid\"; */\n\"cwL-P1-jid.title\" = \"Smart Links\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Quotes\"; ObjectID = \"hQb-2v-fYv\"; */\n\"hQb-2v-fYv.title\" = \"Smart Quotes\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Document Now\"; ObjectID = \"hz2-CU-CR7\"; */\n\"hz2-CU-CR7.title\" = \"Check Document Now\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Grammar With Spelling\"; ObjectID = \"mK6-2p-4JG\"; */\n\"mK6-2p-4JG.title\" = \"Check Grammar With Spelling\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Next\"; ObjectID = \"q09-fT-Sye\"; */\n\"q09-fT-Sye.title\" = \"Find Next\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Spelling While Typing\"; ObjectID = \"rbD-Rh-wIN\"; */\n\"rbD-Rh-wIN.title\" = \"Check Spelling While Typing\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Dashes\"; ObjectID = \"rgM-f4-ycn\"; */\n\"rgM-f4-ycn.title\" = \"Smart Dashes\";\n\n/* Class = \"NSMenuItem\"; title = \"Data Detectors\"; ObjectID = \"tRr-pd-1PS\"; */\n\"tRr-pd-1PS.title\" = \"Data Detectors\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Substitutions\"; ObjectID = \"z6F-FW-3nz\"; */\n\"z6F-FW-3nz.title\" = \"Show Substitutions\";\n"
  },
  {
    "path": "macos/Runner/id.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenu\"; title = \"Find\"; ObjectID = \"1b7-l0-nxx\"; */\n\"1b7-l0-nxx.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Spelling\"; ObjectID = \"3IN-sU-3Bg\"; */\n\"3IN-sU-3Bg.title\" = \"Spelling\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Find\"; ObjectID = \"4EN-yA-p0u\"; */\n\"4EN-yA-p0u.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenuItem\"; title = \"Correct Spelling Automatically\"; ObjectID = \"78Y-hA-62v\"; */\n\"78Y-hA-62v.title\" = \"Correct Spelling Automatically\";\n\n/* Class = \"NSMenuItem\"; title = \"Substitutions\"; ObjectID = \"9ic-FL-obx\"; */\n\"9ic-FL-obx.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Copy/Paste\"; ObjectID = \"9yt-4B-nSM\"; */\n\"9yt-4B-nSM.title\" = \"Smart Copy/Paste\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Preferences…\"; ObjectID = \"BOF-NM-1cW\"; */\n\"BOF-NM-1cW.title\" = \"Preferences…\";\n\n/* Class = \"NSMenuItem\"; title = \"Spelling and Grammar\"; ObjectID = \"Dv1-io-Yv7\"; */\n\"Dv1-io-Yv7.title\" = \"Spelling and Grammar\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"Substitutions\"; ObjectID = \"FeM-D8-WVr\"; */\n\"FeM-D8-WVr.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Text Replacement\"; ObjectID = \"HFQ-gK-NFA\"; */\n\"HFQ-gK-NFA.title\" = \"Text Replacement\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Spelling and Grammar\"; ObjectID = \"HFo-cy-zxI\"; */\n\"HFo-cy-zxI.title\" = \"Show Spelling and Grammar\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Previous\"; ObjectID = \"OwM-mh-QMV\"; */\n\"OwM-mh-QMV.title\" = \"Find Previous\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenuItem\"; title = \"Jump to Selection\"; ObjectID = \"S0p-oC-mLd\"; */\n\"S0p-oC-mLd.title\" = \"Jump to Selection\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste and Match Style\"; ObjectID = \"WeT-3V-zwk\"; */\n\"WeT-3V-zwk.title\" = \"Paste and Match Style\";\n\n/* Class = \"NSMenuItem\"; title = \"Find…\"; ObjectID = \"Xz5-n4-O0W\"; */\n\"Xz5-n4-O0W.title\" = \"Find…\";\n\n/* Class = \"NSMenuItem\"; title = \"Find and Replace…\"; ObjectID = \"YEy-JH-Tfz\"; */\n\"YEy-JH-Tfz.title\" = \"Find and Replace…\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Use Selection for Find\"; ObjectID = \"buJ-ug-pKt\"; */\n\"buJ-ug-pKt.title\" = \"Use Selection for Find\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Links\"; ObjectID = \"cwL-P1-jid\"; */\n\"cwL-P1-jid.title\" = \"Smart Links\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Quotes\"; ObjectID = \"hQb-2v-fYv\"; */\n\"hQb-2v-fYv.title\" = \"Smart Quotes\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Document Now\"; ObjectID = \"hz2-CU-CR7\"; */\n\"hz2-CU-CR7.title\" = \"Check Document Now\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Grammar With Spelling\"; ObjectID = \"mK6-2p-4JG\"; */\n\"mK6-2p-4JG.title\" = \"Check Grammar With Spelling\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Next\"; ObjectID = \"q09-fT-Sye\"; */\n\"q09-fT-Sye.title\" = \"Find Next\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Spelling While Typing\"; ObjectID = \"rbD-Rh-wIN\"; */\n\"rbD-Rh-wIN.title\" = \"Check Spelling While Typing\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Dashes\"; ObjectID = \"rgM-f4-ycn\"; */\n\"rgM-f4-ycn.title\" = \"Smart Dashes\";\n\n/* Class = \"NSMenuItem\"; title = \"Data Detectors\"; ObjectID = \"tRr-pd-1PS\"; */\n\"tRr-pd-1PS.title\" = \"Data Detectors\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Substitutions\"; ObjectID = \"z6F-FW-3nz\"; */\n\"z6F-FW-3nz.title\" = \"Show Substitutions\";\n"
  },
  {
    "path": "macos/Runner/ja.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenu\"; title = \"Find\"; ObjectID = \"1b7-l0-nxx\"; */\n\"1b7-l0-nxx.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Spelling\"; ObjectID = \"3IN-sU-3Bg\"; */\n\"3IN-sU-3Bg.title\" = \"Spelling\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Find\"; ObjectID = \"4EN-yA-p0u\"; */\n\"4EN-yA-p0u.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenuItem\"; title = \"Correct Spelling Automatically\"; ObjectID = \"78Y-hA-62v\"; */\n\"78Y-hA-62v.title\" = \"Correct Spelling Automatically\";\n\n/* Class = \"NSMenuItem\"; title = \"Substitutions\"; ObjectID = \"9ic-FL-obx\"; */\n\"9ic-FL-obx.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Copy/Paste\"; ObjectID = \"9yt-4B-nSM\"; */\n\"9yt-4B-nSM.title\" = \"Smart Copy/Paste\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Preferences…\"; ObjectID = \"BOF-NM-1cW\"; */\n\"BOF-NM-1cW.title\" = \"Preferences…\";\n\n/* Class = \"NSMenuItem\"; title = \"Spelling and Grammar\"; ObjectID = \"Dv1-io-Yv7\"; */\n\"Dv1-io-Yv7.title\" = \"Spelling and Grammar\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"Substitutions\"; ObjectID = \"FeM-D8-WVr\"; */\n\"FeM-D8-WVr.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Text Replacement\"; ObjectID = \"HFQ-gK-NFA\"; */\n\"HFQ-gK-NFA.title\" = \"Text Replacement\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Spelling and Grammar\"; ObjectID = \"HFo-cy-zxI\"; */\n\"HFo-cy-zxI.title\" = \"Show Spelling and Grammar\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Previous\"; ObjectID = \"OwM-mh-QMV\"; */\n\"OwM-mh-QMV.title\" = \"Find Previous\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenuItem\"; title = \"Jump to Selection\"; ObjectID = \"S0p-oC-mLd\"; */\n\"S0p-oC-mLd.title\" = \"Jump to Selection\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste and Match Style\"; ObjectID = \"WeT-3V-zwk\"; */\n\"WeT-3V-zwk.title\" = \"Paste and Match Style\";\n\n/* Class = \"NSMenuItem\"; title = \"Find…\"; ObjectID = \"Xz5-n4-O0W\"; */\n\"Xz5-n4-O0W.title\" = \"Find…\";\n\n/* Class = \"NSMenuItem\"; title = \"Find and Replace…\"; ObjectID = \"YEy-JH-Tfz\"; */\n\"YEy-JH-Tfz.title\" = \"Find and Replace…\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Use Selection for Find\"; ObjectID = \"buJ-ug-pKt\"; */\n\"buJ-ug-pKt.title\" = \"Use Selection for Find\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Links\"; ObjectID = \"cwL-P1-jid\"; */\n\"cwL-P1-jid.title\" = \"Smart Links\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Quotes\"; ObjectID = \"hQb-2v-fYv\"; */\n\"hQb-2v-fYv.title\" = \"Smart Quotes\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Document Now\"; ObjectID = \"hz2-CU-CR7\"; */\n\"hz2-CU-CR7.title\" = \"Check Document Now\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Grammar With Spelling\"; ObjectID = \"mK6-2p-4JG\"; */\n\"mK6-2p-4JG.title\" = \"Check Grammar With Spelling\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Next\"; ObjectID = \"q09-fT-Sye\"; */\n\"q09-fT-Sye.title\" = \"Find Next\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Spelling While Typing\"; ObjectID = \"rbD-Rh-wIN\"; */\n\"rbD-Rh-wIN.title\" = \"Check Spelling While Typing\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Dashes\"; ObjectID = \"rgM-f4-ycn\"; */\n\"rgM-f4-ycn.title\" = \"Smart Dashes\";\n\n/* Class = \"NSMenuItem\"; title = \"Data Detectors\"; ObjectID = \"tRr-pd-1PS\"; */\n\"tRr-pd-1PS.title\" = \"Data Detectors\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Substitutions\"; ObjectID = \"z6F-FW-3nz\"; */\n\"z6F-FW-3nz.title\" = \"Show Substitutions\";\n"
  },
  {
    "path": "macos/Runner/pt-BR.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenu\"; title = \"Find\"; ObjectID = \"1b7-l0-nxx\"; */\n\"1b7-l0-nxx.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Spelling\"; ObjectID = \"3IN-sU-3Bg\"; */\n\"3IN-sU-3Bg.title\" = \"Spelling\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Find\"; ObjectID = \"4EN-yA-p0u\"; */\n\"4EN-yA-p0u.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenuItem\"; title = \"Correct Spelling Automatically\"; ObjectID = \"78Y-hA-62v\"; */\n\"78Y-hA-62v.title\" = \"Correct Spelling Automatically\";\n\n/* Class = \"NSMenuItem\"; title = \"Substitutions\"; ObjectID = \"9ic-FL-obx\"; */\n\"9ic-FL-obx.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Copy/Paste\"; ObjectID = \"9yt-4B-nSM\"; */\n\"9yt-4B-nSM.title\" = \"Smart Copy/Paste\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Preferences…\"; ObjectID = \"BOF-NM-1cW\"; */\n\"BOF-NM-1cW.title\" = \"Preferences…\";\n\n/* Class = \"NSMenuItem\"; title = \"Spelling and Grammar\"; ObjectID = \"Dv1-io-Yv7\"; */\n\"Dv1-io-Yv7.title\" = \"Spelling and Grammar\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"Substitutions\"; ObjectID = \"FeM-D8-WVr\"; */\n\"FeM-D8-WVr.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Text Replacement\"; ObjectID = \"HFQ-gK-NFA\"; */\n\"HFQ-gK-NFA.title\" = \"Text Replacement\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Spelling and Grammar\"; ObjectID = \"HFo-cy-zxI\"; */\n\"HFo-cy-zxI.title\" = \"Show Spelling and Grammar\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Previous\"; ObjectID = \"OwM-mh-QMV\"; */\n\"OwM-mh-QMV.title\" = \"Find Previous\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenuItem\"; title = \"Jump to Selection\"; ObjectID = \"S0p-oC-mLd\"; */\n\"S0p-oC-mLd.title\" = \"Jump to Selection\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste and Match Style\"; ObjectID = \"WeT-3V-zwk\"; */\n\"WeT-3V-zwk.title\" = \"Paste and Match Style\";\n\n/* Class = \"NSMenuItem\"; title = \"Find…\"; ObjectID = \"Xz5-n4-O0W\"; */\n\"Xz5-n4-O0W.title\" = \"Find…\";\n\n/* Class = \"NSMenuItem\"; title = \"Find and Replace…\"; ObjectID = \"YEy-JH-Tfz\"; */\n\"YEy-JH-Tfz.title\" = \"Find and Replace…\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Use Selection for Find\"; ObjectID = \"buJ-ug-pKt\"; */\n\"buJ-ug-pKt.title\" = \"Use Selection for Find\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Links\"; ObjectID = \"cwL-P1-jid\"; */\n\"cwL-P1-jid.title\" = \"Smart Links\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Quotes\"; ObjectID = \"hQb-2v-fYv\"; */\n\"hQb-2v-fYv.title\" = \"Smart Quotes\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Document Now\"; ObjectID = \"hz2-CU-CR7\"; */\n\"hz2-CU-CR7.title\" = \"Check Document Now\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Grammar With Spelling\"; ObjectID = \"mK6-2p-4JG\"; */\n\"mK6-2p-4JG.title\" = \"Check Grammar With Spelling\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Next\"; ObjectID = \"q09-fT-Sye\"; */\n\"q09-fT-Sye.title\" = \"Find Next\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Spelling While Typing\"; ObjectID = \"rbD-Rh-wIN\"; */\n\"rbD-Rh-wIN.title\" = \"Check Spelling While Typing\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Dashes\"; ObjectID = \"rgM-f4-ycn\"; */\n\"rgM-f4-ycn.title\" = \"Smart Dashes\";\n\n/* Class = \"NSMenuItem\"; title = \"Data Detectors\"; ObjectID = \"tRr-pd-1PS\"; */\n\"tRr-pd-1PS.title\" = \"Data Detectors\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Substitutions\"; ObjectID = \"z6F-FW-3nz\"; */\n\"z6F-FW-3nz.title\" = \"Show Substitutions\";\n"
  },
  {
    "path": "macos/Runner/ru.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenu\"; title = \"Find\"; ObjectID = \"1b7-l0-nxx\"; */\n\"1b7-l0-nxx.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Spelling\"; ObjectID = \"3IN-sU-3Bg\"; */\n\"3IN-sU-3Bg.title\" = \"Spelling\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Find\"; ObjectID = \"4EN-yA-p0u\"; */\n\"4EN-yA-p0u.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenuItem\"; title = \"Correct Spelling Automatically\"; ObjectID = \"78Y-hA-62v\"; */\n\"78Y-hA-62v.title\" = \"Correct Spelling Automatically\";\n\n/* Class = \"NSMenuItem\"; title = \"Substitutions\"; ObjectID = \"9ic-FL-obx\"; */\n\"9ic-FL-obx.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Copy/Paste\"; ObjectID = \"9yt-4B-nSM\"; */\n\"9yt-4B-nSM.title\" = \"Smart Copy/Paste\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Preferences…\"; ObjectID = \"BOF-NM-1cW\"; */\n\"BOF-NM-1cW.title\" = \"Preferences…\";\n\n/* Class = \"NSMenuItem\"; title = \"Spelling and Grammar\"; ObjectID = \"Dv1-io-Yv7\"; */\n\"Dv1-io-Yv7.title\" = \"Spelling and Grammar\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"Substitutions\"; ObjectID = \"FeM-D8-WVr\"; */\n\"FeM-D8-WVr.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Text Replacement\"; ObjectID = \"HFQ-gK-NFA\"; */\n\"HFQ-gK-NFA.title\" = \"Text Replacement\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Spelling and Grammar\"; ObjectID = \"HFo-cy-zxI\"; */\n\"HFo-cy-zxI.title\" = \"Show Spelling and Grammar\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Previous\"; ObjectID = \"OwM-mh-QMV\"; */\n\"OwM-mh-QMV.title\" = \"Find Previous\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenuItem\"; title = \"Jump to Selection\"; ObjectID = \"S0p-oC-mLd\"; */\n\"S0p-oC-mLd.title\" = \"Jump to Selection\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste and Match Style\"; ObjectID = \"WeT-3V-zwk\"; */\n\"WeT-3V-zwk.title\" = \"Paste and Match Style\";\n\n/* Class = \"NSMenuItem\"; title = \"Find…\"; ObjectID = \"Xz5-n4-O0W\"; */\n\"Xz5-n4-O0W.title\" = \"Find…\";\n\n/* Class = \"NSMenuItem\"; title = \"Find and Replace…\"; ObjectID = \"YEy-JH-Tfz\"; */\n\"YEy-JH-Tfz.title\" = \"Find and Replace…\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Use Selection for Find\"; ObjectID = \"buJ-ug-pKt\"; */\n\"buJ-ug-pKt.title\" = \"Use Selection for Find\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Links\"; ObjectID = \"cwL-P1-jid\"; */\n\"cwL-P1-jid.title\" = \"Smart Links\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Quotes\"; ObjectID = \"hQb-2v-fYv\"; */\n\"hQb-2v-fYv.title\" = \"Smart Quotes\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Document Now\"; ObjectID = \"hz2-CU-CR7\"; */\n\"hz2-CU-CR7.title\" = \"Check Document Now\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Grammar With Spelling\"; ObjectID = \"mK6-2p-4JG\"; */\n\"mK6-2p-4JG.title\" = \"Check Grammar With Spelling\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Next\"; ObjectID = \"q09-fT-Sye\"; */\n\"q09-fT-Sye.title\" = \"Find Next\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Spelling While Typing\"; ObjectID = \"rbD-Rh-wIN\"; */\n\"rbD-Rh-wIN.title\" = \"Check Spelling While Typing\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Dashes\"; ObjectID = \"rgM-f4-ycn\"; */\n\"rgM-f4-ycn.title\" = \"Smart Dashes\";\n\n/* Class = \"NSMenuItem\"; title = \"Data Detectors\"; ObjectID = \"tRr-pd-1PS\"; */\n\"tRr-pd-1PS.title\" = \"Data Detectors\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Substitutions\"; ObjectID = \"z6F-FW-3nz\"; */\n\"z6F-FW-3nz.title\" = \"Show Substitutions\";\n"
  },
  {
    "path": "macos/Runner/zh-Hans.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenu\"; title = \"Find\"; ObjectID = \"1b7-l0-nxx\"; */\n\"1b7-l0-nxx.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Spelling\"; ObjectID = \"3IN-sU-3Bg\"; */\n\"3IN-sU-3Bg.title\" = \"Spelling\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Find\"; ObjectID = \"4EN-yA-p0u\"; */\n\"4EN-yA-p0u.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenuItem\"; title = \"Correct Spelling Automatically\"; ObjectID = \"78Y-hA-62v\"; */\n\"78Y-hA-62v.title\" = \"Correct Spelling Automatically\";\n\n/* Class = \"NSMenuItem\"; title = \"Substitutions\"; ObjectID = \"9ic-FL-obx\"; */\n\"9ic-FL-obx.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Copy/Paste\"; ObjectID = \"9yt-4B-nSM\"; */\n\"9yt-4B-nSM.title\" = \"Smart Copy/Paste\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Preferences…\"; ObjectID = \"BOF-NM-1cW\"; */\n\"BOF-NM-1cW.title\" = \"Preferences…\";\n\n/* Class = \"NSMenuItem\"; title = \"Spelling and Grammar\"; ObjectID = \"Dv1-io-Yv7\"; */\n\"Dv1-io-Yv7.title\" = \"Spelling and Grammar\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"Substitutions\"; ObjectID = \"FeM-D8-WVr\"; */\n\"FeM-D8-WVr.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Text Replacement\"; ObjectID = \"HFQ-gK-NFA\"; */\n\"HFQ-gK-NFA.title\" = \"Text Replacement\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Spelling and Grammar\"; ObjectID = \"HFo-cy-zxI\"; */\n\"HFo-cy-zxI.title\" = \"Show Spelling and Grammar\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Previous\"; ObjectID = \"OwM-mh-QMV\"; */\n\"OwM-mh-QMV.title\" = \"Find Previous\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenuItem\"; title = \"Jump to Selection\"; ObjectID = \"S0p-oC-mLd\"; */\n\"S0p-oC-mLd.title\" = \"Jump to Selection\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste and Match Style\"; ObjectID = \"WeT-3V-zwk\"; */\n\"WeT-3V-zwk.title\" = \"Paste and Match Style\";\n\n/* Class = \"NSMenuItem\"; title = \"Find…\"; ObjectID = \"Xz5-n4-O0W\"; */\n\"Xz5-n4-O0W.title\" = \"Find…\";\n\n/* Class = \"NSMenuItem\"; title = \"Find and Replace…\"; ObjectID = \"YEy-JH-Tfz\"; */\n\"YEy-JH-Tfz.title\" = \"Find and Replace…\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Use Selection for Find\"; ObjectID = \"buJ-ug-pKt\"; */\n\"buJ-ug-pKt.title\" = \"Use Selection for Find\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Links\"; ObjectID = \"cwL-P1-jid\"; */\n\"cwL-P1-jid.title\" = \"Smart Links\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Quotes\"; ObjectID = \"hQb-2v-fYv\"; */\n\"hQb-2v-fYv.title\" = \"Smart Quotes\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Document Now\"; ObjectID = \"hz2-CU-CR7\"; */\n\"hz2-CU-CR7.title\" = \"Check Document Now\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Grammar With Spelling\"; ObjectID = \"mK6-2p-4JG\"; */\n\"mK6-2p-4JG.title\" = \"Check Grammar With Spelling\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Next\"; ObjectID = \"q09-fT-Sye\"; */\n\"q09-fT-Sye.title\" = \"Find Next\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Spelling While Typing\"; ObjectID = \"rbD-Rh-wIN\"; */\n\"rbD-Rh-wIN.title\" = \"Check Spelling While Typing\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Dashes\"; ObjectID = \"rgM-f4-ycn\"; */\n\"rgM-f4-ycn.title\" = \"Smart Dashes\";\n\n/* Class = \"NSMenuItem\"; title = \"Data Detectors\"; ObjectID = \"tRr-pd-1PS\"; */\n\"tRr-pd-1PS.title\" = \"Data Detectors\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Substitutions\"; ObjectID = \"z6F-FW-3nz\"; */\n\"z6F-FW-3nz.title\" = \"Show Substitutions\";\n"
  },
  {
    "path": "macos/Runner/zh-Hant.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenu\"; title = \"Find\"; ObjectID = \"1b7-l0-nxx\"; */\n\"1b7-l0-nxx.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Spelling\"; ObjectID = \"3IN-sU-3Bg\"; */\n\"3IN-sU-3Bg.title\" = \"Spelling\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Find\"; ObjectID = \"4EN-yA-p0u\"; */\n\"4EN-yA-p0u.title\" = \"Find\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenuItem\"; title = \"Correct Spelling Automatically\"; ObjectID = \"78Y-hA-62v\"; */\n\"78Y-hA-62v.title\" = \"Correct Spelling Automatically\";\n\n/* Class = \"NSMenuItem\"; title = \"Substitutions\"; ObjectID = \"9ic-FL-obx\"; */\n\"9ic-FL-obx.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Copy/Paste\"; ObjectID = \"9yt-4B-nSM\"; */\n\"9yt-4B-nSM.title\" = \"Smart Copy/Paste\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Preferences…\"; ObjectID = \"BOF-NM-1cW\"; */\n\"BOF-NM-1cW.title\" = \"Preferences…\";\n\n/* Class = \"NSMenuItem\"; title = \"Spelling and Grammar\"; ObjectID = \"Dv1-io-Yv7\"; */\n\"Dv1-io-Yv7.title\" = \"Spelling and Grammar\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"Substitutions\"; ObjectID = \"FeM-D8-WVr\"; */\n\"FeM-D8-WVr.title\" = \"Substitutions\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Text Replacement\"; ObjectID = \"HFQ-gK-NFA\"; */\n\"HFQ-gK-NFA.title\" = \"Text Replacement\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Spelling and Grammar\"; ObjectID = \"HFo-cy-zxI\"; */\n\"HFo-cy-zxI.title\" = \"Show Spelling and Grammar\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Previous\"; ObjectID = \"OwM-mh-QMV\"; */\n\"OwM-mh-QMV.title\" = \"Find Previous\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenuItem\"; title = \"Jump to Selection\"; ObjectID = \"S0p-oC-mLd\"; */\n\"S0p-oC-mLd.title\" = \"Jump to Selection\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste and Match Style\"; ObjectID = \"WeT-3V-zwk\"; */\n\"WeT-3V-zwk.title\" = \"Paste and Match Style\";\n\n/* Class = \"NSMenuItem\"; title = \"Find…\"; ObjectID = \"Xz5-n4-O0W\"; */\n\"Xz5-n4-O0W.title\" = \"Find…\";\n\n/* Class = \"NSMenuItem\"; title = \"Find and Replace…\"; ObjectID = \"YEy-JH-Tfz\"; */\n\"YEy-JH-Tfz.title\" = \"Find and Replace…\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Use Selection for Find\"; ObjectID = \"buJ-ug-pKt\"; */\n\"buJ-ug-pKt.title\" = \"Use Selection for Find\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Links\"; ObjectID = \"cwL-P1-jid\"; */\n\"cwL-P1-jid.title\" = \"Smart Links\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Quotes\"; ObjectID = \"hQb-2v-fYv\"; */\n\"hQb-2v-fYv.title\" = \"Smart Quotes\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Document Now\"; ObjectID = \"hz2-CU-CR7\"; */\n\"hz2-CU-CR7.title\" = \"Check Document Now\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Grammar With Spelling\"; ObjectID = \"mK6-2p-4JG\"; */\n\"mK6-2p-4JG.title\" = \"Check Grammar With Spelling\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenuItem\"; title = \"Find Next\"; ObjectID = \"q09-fT-Sye\"; */\n\"q09-fT-Sye.title\" = \"Find Next\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Spelling While Typing\"; ObjectID = \"rbD-Rh-wIN\"; */\n\"rbD-Rh-wIN.title\" = \"Check Spelling While Typing\";\n\n/* Class = \"NSMenuItem\"; title = \"Smart Dashes\"; ObjectID = \"rgM-f4-ycn\"; */\n\"rgM-f4-ycn.title\" = \"Smart Dashes\";\n\n/* Class = \"NSMenuItem\"; title = \"Data Detectors\"; ObjectID = \"tRr-pd-1PS\"; */\n\"tRr-pd-1PS.title\" = \"Data Detectors\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Substitutions\"; ObjectID = \"z6F-FW-3nz\"; */\n\"z6F-FW-3nz.title\" = \"Show Substitutions\";\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\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\tCB890EE4310DAC9F20C4B22D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4428BFC11DD7EDA01918FBB /* Pods_Runner.framework */; };\n\t\tE372ECA42BB94B360078B9D4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E372ECA32BB94B360078B9D4 /* PrivacyInfo.xcprivacy */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\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\t15B02E741770D3594400C89E /* 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\t18C5D7F664081E15D4F73847 /* 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\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 /* Server Box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = \"Server Box.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\t61D014A5BEAA3F969F06061C /* 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\t7E4B8E0435A8CD429FA930EA /* 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\t97047891D5556990B2D8BDF7 /* 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\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\tB4428BFC11DD7EDA01918FBB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tBFB2C4CAFA221419C2D3A5FB /* 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\tC80488AEAD11AE69952819A3 /* 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\tE372ECA32BB94B360078B9D4 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = \"<group>\"; };\n\t\tE3D26BD42B99689700D83425 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/MainMenu.strings\"; sourceTree = \"<group>\"; };\n\t\tE3D26BD52B99689B00D83425 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hant\"; path = \"zh-Hant.lproj/MainMenu.strings\"; sourceTree = \"<group>\"; };\n\t\tE3D26BD62B9968A000D83425 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/MainMenu.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BD72B9968A500D83425 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/MainMenu.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BD82B9968AB00D83425 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/MainMenu.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BD92B9968B200D83425 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/MainMenu.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BDA2B9968B800D83425 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainMenu.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BDB2B9968C400D83425 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/MainMenu.strings; sourceTree = \"<group>\"; };\n\t\tE3D26BDC2B9968D000D83425 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"pt-BR\"; path = \"pt-BR.lproj/MainMenu.strings\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tCB890EE4310DAC9F20C4B22D /* 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\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\tB8A24A574E1520AC66526B2B /* 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 /* Server Box.app */,\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\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\tE372ECA32BB94B360078B9D4 /* PrivacyInfo.xcprivacy */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tB8A24A574E1520AC66526B2B /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t18C5D7F664081E15D4F73847 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\tC80488AEAD11AE69952819A3 /* Pods-Runner.release.xcconfig */,\n\t\t\t\tBFB2C4CAFA221419C2D3A5FB /* Pods-Runner.profile.xcconfig */,\n\t\t\t\t7E4B8E0435A8CD429FA930EA /* Pods-RunnerTests.debug.xcconfig */,\n\t\t\t\t97047891D5556990B2D8BDF7 /* Pods-RunnerTests.release.xcconfig */,\n\t\t\t\t15B02E741770D3594400C89E /* Pods-RunnerTests.profile.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\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\tB4428BFC11DD7EDA01918FBB /* Pods_Runner.framework */,\n\t\t\t\t61D014A5BEAA3F969F06061C /* 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\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\t906584B20D0BBB0E924799BF /* [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\t3EF3AE3CC6FE5ADDF0613960 /* [CP] Embed Pods Frameworks */,\n\t\t\t\tA1B2C3D4E5F60718293A4B5C /* Fix Objective-C Framework Resources */,\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 /* Server Box.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\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\t\"zh-Hans\",\n\t\t\t\t\"zh-Hant\",\n\t\t\t\tfr,\n\t\t\t\tde,\n\t\t\t\tja,\n\t\t\t\tru,\n\t\t\t\tes,\n\t\t\t\tid,\n\t\t\t\t\"pt-BR\",\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\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tE372ECA42BB94B360078B9D4 /* PrivacyInfo.xcprivacy in Resources */,\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};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\tA1B2C3D4E5F60718293A4B5C /* Fix Objective-C Framework 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);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Fix Objective-C Framework Resources\";\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 = \"FRAMEWORK=\\\"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/objective_c.framework\\\"\\nif [ -d \\\"$FRAMEWORK/Versions\\\" ]; then\\n  if [ ! -L \\\"$FRAMEWORK/Versions/Current\\\" ]; then\\n    (cd \\\"$FRAMEWORK/Versions\\\" && ln -sf A Current)\\n  fi\\n  rm -f \\\"$FRAMEWORK/Resources\\\"\\n  ln -sf Versions/Current/Resources \\\"$FRAMEWORK/Resources\\\"\\nfi\\n\";\n\t\t};\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\t3EF3AE3CC6FE5ADDF0613960 /* [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\t906584B20D0BBB0E924799BF /* [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/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\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\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\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\tE3D26BD42B99689700D83425 /* zh-Hans */,\n\t\t\t\tE3D26BD52B99689B00D83425 /* zh-Hant */,\n\t\t\t\tE3D26BD62B9968A000D83425 /* fr */,\n\t\t\t\tE3D26BD72B9968A500D83425 /* de */,\n\t\t\t\tE3D26BD82B9968AB00D83425 /* ja */,\n\t\t\t\tE3D26BD92B9968B200D83425 /* ru */,\n\t\t\t\tE3D26BDA2B9968B800D83425 /* es */,\n\t\t\t\tE3D26BDB2B9968C400D83425 /* id */,\n\t\t\t\tE3D26BDC2B9968D000D83425 /* pt-BR */,\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\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_LOCALIZABILITY_NONLOCALIZED = YES;\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_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\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/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1331;\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = \"Server Box\";\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.0.1331;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;\n\t\t\t\tPRODUCT_NAME = \"Server Box\";\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\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_LOCALIZABILITY_NONLOCALIZED = YES;\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_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\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_LOCALIZABILITY_NONLOCALIZED = YES;\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_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\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 = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1331;\n\t\t\t\tDEVELOPMENT_TEAM = BA88US33G6;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = \"Server Box\";\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.0.1331;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;\n\t\t\t\tPRODUCT_NAME = \"Server Box\";\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 = \"Apple Development\";\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"3rd Party Mac Developer Application\";\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1331;\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\t\"DEVELOPMENT_TEAM[sdk=macosx*]\" = BA88US33G6;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = \"Server Box\";\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.0.1331;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;\n\t\t\t\tPRODUCT_NAME = \"Server Box\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\t\"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]\" = serverbox_lkmm;\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\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\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\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 = \"Server Box.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 = \"Server Box.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 = \"Server Box.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 = \"Server Box.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": "make.dart",
    "content": "#!/usr/bin/env dart\n// ignore_for_file: avoid_print\n\nimport 'dart:convert';\nimport 'dart:io';\n\nconst shellScriptPath = 'lib/data/model/app/scripts/cmd_types.dart';\nconst moreBuildDataPath = 'more_build_data.json';\n\nvoid main(List<String> args) async {\n  final cmd = args.firstOrNull;\n  print('Running make.dart with command: $cmd');\n  switch (cmd) {\n    case 'before':\n      final scriptModCount = await getScriptCommitCount() + 65;\n      final data = {'script': scriptModCount};\n      await File(moreBuildDataPath).writeAsString(json.encode(data));\n      break;\n    case 'after':\n    // Pass\n    default:\n      throw 'Invalid argument: $cmd';\n  }\n}\n\nFuture<int> getScriptCommitCount() async {\n  if (!await File(shellScriptPath).exists()) {\n    print('File not found: $shellScriptPath');\n    exit(1);\n  }\n  final result = await Process.run('git', ['log', '--format=format:%h', shellScriptPath]);\n  return (result.stdout as String).split('\\n').where((line) => line.isNotEmpty).length;\n}\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: server_box\ndescription: server status & toolbox app.\npublish_to: \"none\"\nversion: 1.0.1331+1331\n\nenvironment:\n  sdk: \">=3.11.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n  flutter_localizations:\n    sdk: flutter\n  choice: ^2.3.2\n  crypto: ^3.0.6\n  dio: ^5.2.1\n  dynamic_color: ^1.6.6\n  easy_isolate: ^1.3.0\n  extended_image: ^10.0.0\n  file_picker: ^10.1.9\n  flutter_gbk2utf8: ^1.0.1\n  flutter_riverpod: ^3.0.1\n  flutter_highlight: ^0.7.0\n  flutter_displaymode: ^0.7.0\n  fl_chart: ^1.0.0\n  freezed_annotation: ^3.0.0\n  get_it: ^9.0.5\n  highlight: ^0.7.0\n  hive_ce_flutter: ^2.3.1\n  intl: ^0.20.2\n  json_annotation: ^4.9.0\n  re_editor: ^0.8.0\n  responsive_framework: ^1.5.1\n  riverpod_annotation: ^3.0.1\n  shared_preferences: ^2.1.1\n  wakelock_plus: ^1.2.4\n  wake_on_lan: ^4.1.1+3\n  webdav_client_plus: ^1.0.2\n  xml: ^6.4.2 # for parsing nvidia-smi\n  url_launcher: ^6.2.6\n  dartssh2:\n    path: packages/dartssh2\n  circle_chart:\n    path: packages/circle_chart\n  xterm:\n    path: packages/xterm\n  computer:\n    git:\n      url: https://github.com/lollipopkit/dart_computer\n      ref: v1.0.88\n  watch_connectivity:\n    path: packages/watch_connectivity\n  plain_notification_token:\n    path: packages/plain_notification_token\n  fl_lib:\n    path: packages/fl_lib\n\ndependency_overrides:\n  # webdav_client_plus:\n  #   path: ../webdav_client\n  gtk: # TODO: remove it after fixed in upstream\n    git:\n      url: https://github.com/lollipopkit/gtk.dart\n      ref: v0.0.36\n\ndev_dependencies:\n  analyzer: ^8.1.1\n  flutter_native_splash: ^2.1.6\n  hive_ce_generator: ^1.9.2\n  build_runner: ^2.4.15\n  flutter_lints: ^6.0.0\n  json_serializable: ^6.8.0\n  freezed: ^3.0.0\n  riverpod_generator: ^3.0.1\n  test: ^1.24.0\n  flutter_test:\n    sdk: flutter\n  # riverpod_reg:\n  #   git:\n  #     url: https://github.com/lollipopkit/riverpod_reg\n  #     ref: v0.0.2\n  fl_build:\n    path: packages/fl_build\n\nflutter:\n  generate: true\n\n  uses-material-design: true\n\n  # To add assets to your application, add an assets section, like this:\n  assets:\n    - assets/app_icon.png\n  #   - images/a_dot_burr.jpeg\n  #   - images/a_dot_ham.jpeg\n  # fonts:\n  #   - family: Schyler\n  #     fonts:\n  #       - asset: fonts/Schyler-Regular.ttf\n  #       - asset: fonts/Schyler-Italic.ttf\n  #         style: italic\n  #   - family: Trajan Pro\n  #     fonts:\n  #       - asset: fonts/TrajanPro.ttf\n  #       - asset: fonts/TrajanPro_Bold.ttf\n  #         weight: 700\n\nriverpod_reg:\n  class_name: Riverpods\n  gen_path: lib/generated/riverpod_reg.dart\n\nflutter_native_splash:\n  color: \"#ffffff\"\n  image: assets/app_icon.png\n  color_dark: \"#121212\"\n  info_plist_files:\n    - \"ios/Runner/Info-Debug.plist\"\n    - \"ios/Runner/Info-Profile.plist\"\n    - \"ios/Runner/Info-Release.plist\"\n  android12: true\n"
  },
  {
    "path": "test/amd_smi_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/amd.dart';\n\nconst _amdSmiRaw = '''\n[\n  {\n    \"name\": \"AMD Radeon RX 7900 XTX\",\n    \"device_id\": \"0\",\n    \"temp\": 45,\n    \"power_draw\": 120,\n    \"power_cap\": 355,\n    \"memory\": {\n      \"total\": 24576,\n      \"used\": 1024,\n      \"unit\": \"MB\",\n      \"processes\": [\n        {\n          \"pid\": 2456,\n          \"name\": \"firefox\",\n          \"memory\": 512\n        },\n        {\n          \"pid\": 3784,\n          \"name\": \"blender\",\n          \"memory\": 256\n        }\n      ]\n    },\n    \"utilization\": 75,\n    \"fan_speed\": 1200,\n    \"clock_speed\": 2400\n  },\n  {\n    \"name\": \"AMD Radeon RX 6800 XT\",\n    \"device_id\": \"1\",\n    \"temp\": 38,\n    \"power_draw\": 85,\n    \"power_cap\": 300,\n    \"memory\": {\n      \"total\": 16384,\n      \"used\": 512,\n      \"unit\": \"MB\",\n      \"processes\": []\n    },\n    \"utilization\": 25,\n    \"fan_speed\": 800,\n    \"clock_speed\": 2100\n  }\n]\n''';\n\nconst _amdSmiRocmRaw = '''\n[\n  {\n    \"card_model\": \"AMD Radeon RX 6700 XT\",\n    \"gpu_id\": \"card0\",\n    \"temperature\": \"42°C\",\n    \"power_draw\": \"95\",\n    \"power_cap\": \"230\",\n    \"vram\": {\n      \"total_memory\": 12288,\n      \"used_memory\": 768,\n      \"unit\": \"MiB\",\n      \"processes\": [\n        {\n          \"pid\": 1234,\n          \"process_name\": \"game.exe\",\n          \"used_memory\": 512\n        }\n      ]\n    },\n    \"gpu_util\": \"60%\",\n    \"fan_rpm\": \"950 RPM\",\n    \"sclk\": \"1800MHz\"\n  }\n]\n''';\n\nconst _amdSmiAlternativeRaw = '''\n[\n  {\n    \"device_name\": \"Radeon RX 580\",\n    \"gpu_temp\": 55,\n    \"current_power\": 150,\n    \"power_limit\": 185,\n    \"memory\": {\n      \"total\": 8192,\n      \"used\": 2048,\n      \"unit\": \"MB\"\n    },\n    \"activity\": 90,\n    \"fan_speed\": 1500,\n    \"gpu_clock\": 1366\n  }\n]\n''';\n\nconst _amdSmiEdgeCasesRaw = '''\n[\n  {\n    \"name\": \"Unknown AMD GPU\",\n    \"device_id\": \"\",\n    \"temp\": null,\n    \"power\": null,\n    \"memory\": {},\n    \"utilization\": null,\n    \"fan_speed\": null,\n    \"clock_speed\": null\n  },\n  {\n    \"name\": \"AMD Test GPU\",\n    \"device_id\": \"test\",\n    \"temp\": \"50°C\",\n    \"power_draw\": 100,\n    \"memory\": {\n      \"total\": \"16384MB\",\n      \"used\": \"2048MB\"\n    },\n    \"utilization\": \"80%\",\n    \"fan_speed\": \"1100 RPM\",\n    \"clock_speed\": \"2000 MHz\"\n  }\n]\n''';\n\nconst _invalidJson = '''\n{\n  \"invalid\": \"not an array\"\n}\n''';\n\nconst _emptyArray = '[]';\n\nconst _malformedJson = '''\n[\n  {\n    \"name\": \"Test GPU\"\n    // missing closing brace\n''';\n\nvoid main() {\n  group('AmdSmi JSON parsing', () {\n    test('parse standard AMD SMI output', () {\n      final gpus = AmdSmi.fromJson(_amdSmiRaw);\n      expect(gpus.length, 2);\n\n      final gpu1 = gpus[0];\n      expect(gpu1.name, 'AMD Radeon RX 7900 XTX');\n      expect(gpu1.deviceId, '0');\n      expect(gpu1.temp, 45);\n      expect(gpu1.power, '120W / 355W');\n      expect(gpu1.memory.total, 24576);\n      expect(gpu1.memory.used, 1024);\n      expect(gpu1.memory.unit, 'MB');\n      expect(gpu1.memory.processes.length, 2);\n      expect(gpu1.memory.processes[0].pid, 2456);\n      expect(gpu1.memory.processes[0].name, 'firefox');\n      expect(gpu1.memory.processes[0].memory, 512);\n      expect(gpu1.memory.processes[1].pid, 3784);\n      expect(gpu1.memory.processes[1].name, 'blender');\n      expect(gpu1.memory.processes[1].memory, 256);\n      expect(gpu1.utilization, 75);\n      expect(gpu1.fanSpeed, 1200);\n      expect(gpu1.clockSpeed, 2400);\n\n      final gpu2 = gpus[1];\n      expect(gpu2.name, 'AMD Radeon RX 6800 XT');\n      expect(gpu2.deviceId, '1');\n      expect(gpu2.temp, 38);\n      expect(gpu2.power, '85W / 300W');\n      expect(gpu2.memory.total, 16384);\n      expect(gpu2.memory.used, 512);\n      expect(gpu2.memory.unit, 'MB');\n      expect(gpu2.memory.processes.length, 0);\n      expect(gpu2.utilization, 25);\n      expect(gpu2.fanSpeed, 800);\n      expect(gpu2.clockSpeed, 2100);\n    });\n\n    test('parse ROCm SMI output with different field names', () {\n      final gpus = AmdSmi.fromJson(_amdSmiRocmRaw);\n      expect(gpus.length, 1);\n\n      final gpu = gpus[0];\n      expect(gpu.name, 'AMD Radeon RX 6700 XT');\n      expect(gpu.deviceId, 'card0');\n      expect(gpu.temp, 42);\n      expect(gpu.power, '95W / 230W');\n      expect(gpu.memory.total, 12288);\n      expect(gpu.memory.used, 768);\n      expect(gpu.memory.unit, 'MiB');\n      expect(gpu.memory.processes.length, 1);\n      expect(gpu.memory.processes[0].pid, 1234);\n      expect(gpu.memory.processes[0].name, 'game.exe');\n      expect(gpu.memory.processes[0].memory, 512);\n      expect(gpu.utilization, 60);\n      expect(gpu.fanSpeed, 950);\n      expect(gpu.clockSpeed, 1800);\n    });\n\n    test('parse alternative field names', () {\n      final gpus = AmdSmi.fromJson(_amdSmiAlternativeRaw);\n      expect(gpus.length, 1);\n\n      final gpu = gpus[0];\n      expect(gpu.name, 'Radeon RX 580');\n      expect(gpu.deviceId, '0');\n      expect(gpu.temp, 55);\n      expect(gpu.power, '150W / 185W');\n      expect(gpu.memory.total, 8192);\n      expect(gpu.memory.used, 2048);\n      expect(gpu.memory.unit, 'MB');\n      expect(gpu.memory.processes.length, 0);\n      expect(gpu.utilization, 90);\n      expect(gpu.fanSpeed, 1500);\n      expect(gpu.clockSpeed, 1366);\n    });\n\n    test('handle edge cases and string parsing', () {\n      final gpus = AmdSmi.fromJson(_amdSmiEdgeCasesRaw);\n      expect(gpus.length, 2);\n\n      final gpu1 = gpus[0];\n      expect(gpu1.name, 'Unknown AMD GPU');\n      expect(gpu1.deviceId, '');\n      expect(gpu1.temp, 0);\n      expect(gpu1.power, 'N/A');\n      expect(gpu1.memory.total, 0);\n      expect(gpu1.memory.used, 0);\n      expect(gpu1.memory.unit, 'MB');\n      expect(gpu1.memory.processes.length, 0);\n      expect(gpu1.utilization, 0);\n      expect(gpu1.fanSpeed, 0);\n      expect(gpu1.clockSpeed, 0);\n\n      final gpu2 = gpus[1];\n      expect(gpu2.name, 'AMD Test GPU');\n      expect(gpu2.deviceId, 'test');\n      expect(gpu2.temp, 50);\n      expect(gpu2.power, '100W');\n      expect(gpu2.memory.total, 16384);\n      expect(gpu2.memory.used, 2048);\n      expect(gpu2.memory.unit, 'MB');\n      expect(gpu2.utilization, 80);\n      expect(gpu2.fanSpeed, 1100);\n      expect(gpu2.clockSpeed, 2000);\n    });\n\n    test('handle invalid JSON gracefully', () {\n      final gpus1 = AmdSmi.fromJson(_invalidJson);\n      expect(gpus1.length, 0);\n\n      final gpus2 = AmdSmi.fromJson(_malformedJson);\n      expect(gpus2.length, 0);\n\n      final gpus3 = AmdSmi.fromJson('invalid json');\n      expect(gpus3.length, 0);\n    });\n\n    test('handle empty array', () {\n      final gpus = AmdSmi.fromJson(_emptyArray);\n      expect(gpus.length, 0);\n    });\n  });\n\n  group('AmdSmi helper methods', () {\n    test('_parseIntValue handles various input types', () {\n      expect(AmdSmi.fromJson('[{\"name\":\"test\",\"temp\":42}]')[0].temp, 42);\n      expect(AmdSmi.fromJson('[{\"name\":\"test\",\"temp\":\"45°C\"}]')[0].temp, 45);\n      expect(AmdSmi.fromJson('[{\"name\":\"test\",\"temp\":\"1200 RPM\"}]')[0].temp, 1200);\n      expect(AmdSmi.fromJson('[{\"name\":\"test\",\"temp\":\"N/A\"}]')[0].temp, 0);\n      expect(AmdSmi.fromJson('[{\"name\":\"test\",\"temp\":null}]')[0].temp, 0);\n    });\n\n    test('_formatPower handles different power scenarios', () {\n      final gpu1 = AmdSmi.fromJson('[{\"name\":\"test\",\"power_draw\":100,\"power_cap\":200}]')[0];\n      expect(gpu1.power, '100W / 200W');\n\n      final gpu2 = AmdSmi.fromJson('[{\"name\":\"test\",\"power_draw\":50}]')[0];\n      expect(gpu2.power, '50W');\n\n      final gpu3 = AmdSmi.fromJson('[{\"name\":\"test\"}]')[0];\n      expect(gpu3.power, 'N/A');\n    });\n\n    test('_parseMemory handles missing memory data', () {\n      final gpu = AmdSmi.fromJson('[{\"name\":\"test\"}]')[0];\n      expect(gpu.memory.total, 0);\n      expect(gpu.memory.used, 0);\n      expect(gpu.memory.unit, 'MB');\n      expect(gpu.memory.processes.length, 0);\n    });\n\n    test('_parseProcess filters invalid processes', () {\n      const jsonWithInvalidProcess = '''\n      [\n        {\n          \"name\": \"Test GPU\",\n          \"memory\": {\n            \"processes\": [\n              {\n                \"pid\": 0,\n                \"name\": \"invalid\",\n                \"memory\": 100\n              },\n              {\n                \"pid\": 1234,\n                \"name\": \"valid\",\n                \"memory\": 200\n              }\n            ]\n          }\n        }\n      ]\n      ''';\n\n      final gpu = AmdSmi.fromJson(jsonWithInvalidProcess)[0];\n      expect(gpu.memory.processes.length, 1);\n      expect(gpu.memory.processes[0].pid, 1234);\n      expect(gpu.memory.processes[0].name, 'valid');\n    });\n  });\n\n  group('AmdSmi data classes', () {\n    test('AmdSmiItem toString', () {\n      final memory = AmdSmiMem(8192, 2048, 'MB', []);\n      final item = AmdSmiItem(\n        deviceId: '0',\n        name: 'Test GPU',\n        temp: 45,\n        power: '100W / 200W',\n        memory: memory,\n        utilization: 75,\n        fanSpeed: 1200,\n        clockSpeed: 2400,\n      );\n\n      final toString = item.toString();\n      expect(toString, contains('Test GPU'));\n      expect(toString, contains('45'));\n      expect(toString, contains('100W / 200W'));\n      expect(toString, contains('75%'));\n    });\n\n    test('AmdSmiMem toString', () {\n      final process = AmdSmiMemProcess(1234, 'test', 512);\n      final memory = AmdSmiMem(8192, 2048, 'MB', [process]);\n\n      final toString = memory.toString();\n      expect(toString, contains('8192'));\n      expect(toString, contains('2048'));\n      expect(toString, contains('MB'));\n      expect(toString, contains('1'));\n    });\n\n    test('AmdSmiMemProcess toString', () {\n      final process = AmdSmiMemProcess(1234, 'firefox', 512);\n\n      final toString = process.toString();\n      expect(toString, contains('1234'));\n      expect(toString, contains('firefox'));\n      expect(toString, contains('512'));\n    });\n  });\n\n  group('AmdSmi robustness', () {\n    test('handles malformed GPU objects gracefully', () {\n      const malformedGpuJson = '''\n      [\n        {\n          \"name\": \"Valid GPU\",\n          \"temp\": 45\n        },\n        {\n          \"malformed\": true\n        },\n        {\n          \"name\": \"Another Valid GPU\",\n          \"temp\": 50\n        }\n      ]\n      ''';\n\n      final gpus = AmdSmi.fromJson(malformedGpuJson);\n      expect(gpus.length, 3);\n      expect(gpus[0].name, 'Valid GPU');\n      expect(gpus[0].temp, 45);\n      expect(gpus[1].name, 'Unknown AMD GPU');\n      expect(gpus[1].temp, 0);\n      expect(gpus[2].name, 'Another Valid GPU');\n      expect(gpus[2].temp, 50);\n    });\n\n    test('handles missing required fields with defaults', () {\n      const minimalGpuJson = '''\n      [\n        {}\n      ]\n      ''';\n\n      final gpus = AmdSmi.fromJson(minimalGpuJson);\n      expect(gpus.length, 1);\n      expect(gpus[0].name, 'Unknown AMD GPU');\n      expect(gpus[0].deviceId, '0');\n      expect(gpus[0].temp, 0);\n      expect(gpus[0].power, 'N/A');\n      expect(gpus[0].utilization, 0);\n      expect(gpus[0].fanSpeed, 0);\n      expect(gpus[0].clockSpeed, 0);\n    });\n  });\n}\n"
  },
  {
    "path": "test/battery_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/battery.dart';\n\nconst _raw = '''\nPOWER_SUPPLY_NAME=battery\nPOWER_SUPPLY_INPUT_SUSPEND=0\nPOWER_SUPPLY_BATTERY_INPUT_SUSPEND=0\nPOWER_SUPPLY_STATUS=Discharging\nPOWER_SUPPLY_HEALTH=Good\nPOWER_SUPPLY_PRESENT=1\nPOWER_SUPPLY_CHARGE_TYPE=N/A\nPOWER_SUPPLY_CAPACITY=73\nPOWER_SUPPLY_CHARGER_TEMP_MAX=800\nPOWER_SUPPLY_INPUT_CURRENT_LIMITED=0\nPOWER_SUPPLY_VOLTAGE_NOW=4002675\nPOWER_SUPPLY_VOLTAGE_MAX=4480000\nPOWER_SUPPLY_VOLTAGE_QNOVO=-22\nPOWER_SUPPLY_CURRENT_NOW=157714\nPOWER_SUPPLY_CURRENT_QNOVO=-22\nPOWER_SUPPLY_CONSTANT_CHARGE_CURRENT_MAX=6000000\nPOWER_SUPPLY_CONSTANT_CHARGE_CURRENT=4600000\nPOWER_SUPPLY_CHARGE_TERM_CURRENT=-220\nPOWER_SUPPLY_TEMP=235\nPOWER_SUPPLY_TECHNOLOGY=Li-poly\nPOWER_SUPPLY_STEP_CHARGING_ENABLED=0\nPOWER_SUPPLY_SW_JEITA_ENABLED=1\nPOWER_SUPPLY_CHARGE_DONE=0\nPOWER_SUPPLY_PARALLEL_DISABLE=0\nPOWER_SUPPLY_SET_SHIP_MODE=0\nPOWER_SUPPLY_DIE_HEALTH=Unknown\nPOWER_SUPPLY_DC_THERMAL_LEVELS=0\nPOWER_SUPPLY_RERUN_AICL=0\nPOWER_SUPPLY_DP_DM=0\nPOWER_SUPPLY_CHARGE_CONTROL_LIMIT_MAX=16\nPOWER_SUPPLY_CHARGE_CONTROL_LIMIT=0\nPOWER_SUPPLY_CHARGE_COUNTER=2033341\nPOWER_SUPPLY_CYCLE_COUNT=1\nPOWER_SUPPLY_RECHARGE_SOC=99\nPOWER_SUPPLY_RECHARGE_VBAT=-22\nPOWER_SUPPLY_NIGHT_CHARGING=0\nPOWER_SUPPLY_CHARGE_FULL=3022000\nPOWER_SUPPLY_FORCE_RECHARGE=0\nPOWER_SUPPLY_CHARGE_FULL_DESIGN=4700000\nPOWER_SUPPLY_FCC_STEPPER_ENABLE=0\nPOWER_SUPPLY_BATTERY_CHARGING_ENABLED=1\nPOWER_SUPPLY_DP_DM_BQ=0\nPOWER_SUPPLY_TYPE_RECHECK=0\nPOWER_SUPPLY_WARM_FAKE_CHARGING=0\nPOWER_SUPPLY_STEP_VFLOAT_INDEX=0\nPOWER_SUPPLY_CAPACITY_LEVEL=Normal\n\nPOWER_SUPPLY_NAME=bms\nPOWER_SUPPLY_AUTHENTIC=1\nPOWER_SUPPLY_ROMID=9f,e8,41,99,10,f0,04,42\nPOWER_SUPPLY_DS_STATUS=00,00,08,03,02,00,ff,e8\nPOWER_SUPPLY_PAGE0_DATA=53,42,4d,34,51,30,33,32,00,3e,b0,46,4d,44,31,31\nPOWER_SUPPLY_CHIP_OK=1\nPOWER_SUPPLY_CAPACITY=73\nPOWER_SUPPLY_REAL_CAPACITY=73\nPOWER_SUPPLY_SHUTDOWN_DELAY=0\nPOWER_SUPPLY_CAPACITY_RAW=7340\nPOWER_SUPPLY_SOC_DECIMAL=40\nPOWER_SUPPLY_SOC_DECIMAL_RATE=30\nPOWER_SUPPLY_CC_SOC=6318\nPOWER_SUPPLY_TEMP=235\nPOWER_SUPPLY_VOLTAGE_NOW=4002675\nPOWER_SUPPLY_VOLTAGE_OCV=4039556\nPOWER_SUPPLY_VOLTAGE_AVG=3989287\nPOWER_SUPPLY_CURRENT_NOW=157714\nPOWER_SUPPLY_CURRENT_AVG=329915\nPOWER_SUPPLY_RESISTANCE_ID=100700\nPOWER_SUPPLY_RESISTANCE=186278\nPOWER_SUPPLY_ESR_ACTUAL=-22\nPOWER_SUPPLY_ESR_NOMINAL=-22\nPOWER_SUPPLY_BATTERY_TYPE=j11sun_4700mah\nPOWER_SUPPLY_CHARGE_FULL_DESIGN=4700000\nPOWER_SUPPLY_VOLTAGE_MAX_DESIGN=4450000\nPOWER_SUPPLY_CHARGE_NOW_RAW=2974065\nPOWER_SUPPLY_CHARGE_NOW=0\nPOWER_SUPPLY_CHARGE_FULL=3022000\nPOWER_SUPPLY_CHARGE_COUNTER=2033341\nPOWER_SUPPLY_CHARGE_COUNTER_SHADOW=2033435\nPOWER_SUPPLY_CYCLE_COUNT=1\nPOWER_SUPPLY_CYCLE_COUNTS=15 335 526 552 493 398 287 141 \nPOWER_SUPPLY_SOC_REPORTING_READY=1\nPOWER_SUPPLY_CLEAR_SOH=0\nPOWER_SUPPLY_SOH=50\nPOWER_SUPPLY_DEBUG_BATTERY=0\nPOWER_SUPPLY_CONSTANT_CHARGE_VOLTAGE=4439948\nPOWER_SUPPLY_TIME_TO_FULL_AVG=-1\nPOWER_SUPPLY_TIME_TO_EMPTY_AVG=18366\nPOWER_SUPPLY_CC_STEP=0\nPOWER_SUPPLY_CC_STEP_SEL=0\nPOWER_SUPPLY_BATT_AGE_LEVEL=-22\nPOWER_SUPPLY_POWER_NOW=26828590\nPOWER_SUPPLY_POWER_AVG=16967163\nPOWER_SUPPLY_SCALE_MODE_EN=0\nPOWER_SUPPLY_CALIBRATE=-22\nPOWER_SUPPLY_FASTCHARGE_MODE=0\nPOWER_SUPPLY_FFC_TERMINATION_CURRENT=-800\nPOWER_SUPPLY_SYS_TERMINATION_CURRENT=-300\nPOWER_SUPPLY_FFC_SYS_TERMINATION_CURRENT=-900\nPOWER_SUPPLY_VBATT_FULL_VOL=4440\nPOWER_SUPPLY_FCC_VBATT_FULL_VOL=4490\nPOWER_SUPPLY_KI_COEFF_CURRENT=3900\n\nPOWER_SUPPLY_NAME=bq2597x-standalone\nPOWER_SUPPLY_PRESENT=0\nPOWER_SUPPLY_CHARGING_ENABLED=0\nPOWER_SUPPLY_STATUS=Unknown\nPOWER_SUPPLY_TI_BATTERY_PRESENT=1\nPOWER_SUPPLY_TI_VBUS_PRESENT=0\nPOWER_SUPPLY_TI_BATTERY_VOLTAGE=4004\nPOWER_SUPPLY_TI_BATTERY_CURRENT=0\nPOWER_SUPPLY_TI_BATTERY_TEMPERATURE=0\nPOWER_SUPPLY_TI_BUS_VOLTAGE=0\nPOWER_SUPPLY_TI_BUS_CURRENT=0\nPOWER_SUPPLY_TI_BUS_TEMPERATURE=0\nPOWER_SUPPLY_TI_DIE_TEMPERATURE=0\nPOWER_SUPPLY_TI_ALARM_STATUS=0\nPOWER_SUPPLY_TI_FAULT_STATUS=0\nPOWER_SUPPLY_TI_REG_STATUS=0\nPOWER_SUPPLY_TI_SET_BUS_PROTECTION_FOR_QC3=0\nPOWER_SUPPLY_MODEL_NAME=bq2597x-standalone\n\nPOWER_SUPPLY_NAME=dc\nPOWER_SUPPLY_INPUT_SUSPEND=0\nPOWER_SUPPLY_PRESENT=0\nPOWER_SUPPLY_ONLINE=0\nPOWER_SUPPLY_CURRENT_MAX=100000\nPOWER_SUPPLY_VOLTAGE_MAX=12000000\nPOWER_SUPPLY_REAL_TYPE=Wireless\nPOWER_SUPPLY_DC_RESET=0\nPOWER_SUPPLY_AICL_DONE=0\n\nPOWER_SUPPLY_NAME=main\nPOWER_SUPPLY_VOLTAGE_MAX=4450000\nPOWER_SUPPLY_CONSTANT_CHARGE_CURRENT_MAX=4600000\nPOWER_SUPPLY_TYPE=Main\nPOWER_SUPPLY_INPUT_CURRENT_SETTLED=0\nPOWER_SUPPLY_INPUT_VOLTAGE_SETTLED=5000000\nPOWER_SUPPLY_FCC_DELTA=0\nPOWER_SUPPLY_CURRENT_MAX=0\nPOWER_SUPPLY_FLASH_ACTIVE=0\nPOWER_SUPPLY_FLASH_TRIGGER=0\nPOWER_SUPPLY_TOGGLE_STAT=0\nPOWER_SUPPLY_MAIN_FCC_MAX=-22\nPOWER_SUPPLY_IRQ_STATUS=0\nPOWER_SUPPLY_FORCE_MAIN_FCC=4600000\nPOWER_SUPPLY_FORCE_MAIN_ICL=100000\nPOWER_SUPPLY_COMP_CLAMP_LEVEL=0\nPOWER_SUPPLY_HEALTH=Unknown\nPOWER_SUPPLY_TEMP_HOT=0\n\nPOWER_SUPPLY_NAME=pc_port\nPOWER_SUPPLY_TYPE=USB\nPOWER_SUPPLY_ONLINE=0\nPOWER_SUPPLY_VOLTAGE_MAX=5000000\nPOWER_SUPPLY_CURRENT_MAX=0\n\nPOWER_SUPPLY_NAME=usb\nPOWER_SUPPLY_PRESENT=0\nPOWER_SUPPLY_ONLINE=0\nPOWER_SUPPLY_VOLTAGE_NOW=0\nPOWER_SUPPLY_PD_CURRENT_MAX=-22\nPOWER_SUPPLY_CURRENT_MAX=0\nPOWER_SUPPLY_TYPE=USB_PD\nPOWER_SUPPLY_TYPEC_MODE=Nothing attached\nPOWER_SUPPLY_TYPEC_POWER_ROLE=dual power role\nPOWER_SUPPLY_TYPEC_CC_ORIENTATION=0\nPOWER_SUPPLY_PD_ACTIVE=0\nPOWER_SUPPLY_INPUT_CURRENT_SETTLED=0\nPOWER_SUPPLY_INPUT_CURRENT_NOW=0\nPOWER_SUPPLY_BOOST_CURRENT=0\nPOWER_SUPPLY_PE_START=0\nPOWER_SUPPLY_CTM_CURRENT_MAX=-22\nPOWER_SUPPLY_HW_CURRENT_MAX=0\nPOWER_SUPPLY_REAL_TYPE=Unknown\nPOWER_SUPPLY_HVDCP3_TYPE=0\nPOWER_SUPPLY_QUICK_CHARGE_TYPE=0\nPOWER_SUPPLY_PD_VOLTAGE_MAX=5000000\nPOWER_SUPPLY_PD_VOLTAGE_MIN=5000000\nPOWER_SUPPLY_CONNECTOR_TYPE=0\nPOWER_SUPPLY_CONNECTOR_HEALTH=Cool\nPOWER_SUPPLY_CONNECTOR_TEMP=293\nPOWER_SUPPLY_VBUS_DISABLE=0\nPOWER_SUPPLY_VOLTAGE_MAX=5000000\nPOWER_SUPPLY_VOLTAGE_MAX_DESIGN=5000000\nPOWER_SUPPLY_VOLTAGE_MAX_LIMIT=5000000\nPOWER_SUPPLY_SMB_EN_MODE=0\nPOWER_SUPPLY_SMB_EN_REASON=0\nPOWER_SUPPLY_ADAPTER_CC_MODE=0\nPOWER_SUPPLY_SCOPE=Unknown\nPOWER_SUPPLY_MOISTURE_DETECTED=0\nPOWER_SUPPLY_HVDCP_OPTI_ALLOWED=1\nPOWER_SUPPLY_QC_OPTI_DISABLE=0\nPOWER_SUPPLY_VOLTAGE_VPH=3995694\nPOWER_SUPPLY_THERM_ICL_LIMIT=-22\nPOWER_SUPPLY_FASTCHARGE_MODE=0\nPOWER_SUPPLY_PD_AUTHENTICATION=0\nPOWER_SUPPLY_SKIN_HEALTH=Unknown\nPOWER_SUPPLY_APSD_RERUN=0\nPOWER_SUPPLY_APSD_TIMEOUT=0\nPOWER_SUPPLY_APDO_MAX=0\nPOWER_SUPPLY_CHARGER_STATUS=0\nPOWER_SUPPLY_INPUT_VOLTAGE_SETTLED=0\nPOWER_SUPPLY_MOISTURE_DETECTION_ENABLED=0\n''';\n\nvoid main() {\n  test('parse battery', () {\n    final result = Batteries.parse(_raw);\n    expect(result.length, 7);\n  });\n}\n"
  },
  {
    "path": "test/btrfs_test.dart",
    "content": "// ignore_for_file: avoid_print\n\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/disk.dart';\n\nvoid main() {\n  group('BTRFS RAID1 disk parsing', () {\n    test('correctly handles BTRFS RAID1 with same UUID', () {\n      final disks = Disk.parse(_btrfsRaidJsonOutput);\n      expect(disks, isNotEmpty);\n      expect(disks.length, 4); // Should have 2 parent disks + 2 BTRFS partitions\n\n      // We should get two distinct disks with the same UUID but different paths\n      final nvme1Disk = disks.firstWhere((disk) => disk.path == '/dev/nvme1n1p1');\n      final nvme2Disk = disks.firstWhere((disk) => disk.path == '/dev/nvme2n1p1');\n\n      // Both should exist\n      expect(nvme1Disk, isNotNull);\n      expect(nvme2Disk, isNotNull);\n\n      // They should have the same UUID (since they're part of the same BTRFS volume)\n      expect(nvme1Disk.uuid, nvme2Disk.uuid);\n\n      // But they should be treated as distinct disks\n      expect(identical(nvme1Disk, nvme2Disk), isFalse);\n\n      // Verify DiskUsage counts physical disks correctly\n      final usage = DiskUsage.parse(disks);\n      // With our unique path+kname identifier, both disks should be counted\n      expect(usage.size, nvme1Disk.size + nvme2Disk.size);\n      expect(usage.used, nvme1Disk.used + nvme2Disk.used);\n    });\n  });\n}\n\n// Simulated BTRFS RAID1 lsblk JSON output\nconst _btrfsRaidJsonOutput = '''\n{\n  \"blockdevices\": [\n    {\n      \"name\": \"nvme1n1\",\n      \"kname\": \"nvme1n1\",\n      \"path\": \"/dev/nvme1n1\",\n      \"fstype\": null,\n      \"mountpoint\": null,\n      \"fssize\": null,\n      \"fsused\": null,\n      \"fsavail\": null,\n      \"fsuse%\": null,\n      \"children\": [\n        {\n          \"name\": \"nvme1n1p1\",\n          \"kname\": \"nvme1n1p1\",\n          \"path\": \"/dev/nvme1n1p1\",\n          \"fstype\": \"btrfs\",\n          \"mountpoint\": \"/mnt/raid\",\n          \"fssize\": \"500000000000\",\n          \"fsused\": \"100000000000\",\n          \"fsavail\": \"400000000000\",\n          \"fsuse%\": \"20%\",\n          \"uuid\": \"btrfs-raid-uuid-1234-5678\"\n        }\n      ]\n    },\n    {\n      \"name\": \"nvme2n1\",\n      \"kname\": \"nvme2n1\",\n      \"path\": \"/dev/nvme2n1\",\n      \"fstype\": null,\n      \"mountpoint\": null,\n      \"fssize\": null,\n      \"fsused\": null,\n      \"fsavail\": null,\n      \"fsuse%\": null,\n      \"children\": [\n        {\n          \"name\": \"nvme2n1p1\",\n          \"kname\": \"nvme2n1p1\",\n          \"path\": \"/dev/nvme2n1p1\",\n          \"fstype\": \"btrfs\",\n          \"mountpoint\": \"/mnt/raid\",\n          \"fssize\": \"500000000000\",\n          \"fsused\": \"100000000000\",\n          \"fsavail\": \"400000000000\",\n          \"fsuse%\": \"20%\",\n          \"uuid\": \"btrfs-raid-uuid-1234-5678\"\n        }\n      ]\n    }\n  ]\n}\n''';\n"
  },
  {
    "path": "test/container_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/container/ps.dart';\nimport 'package:server_box/data/model/container/status.dart';\n\nvoid main() {\n  test('docker ps parse', () {\n    const raw = '''\nCONTAINER ID    STATUS                         NAMES                                              IMAGE                                              \n0e9e2ef860d2    Up 2 hours                     hbbs                                               rustdesk/rustdesk-server:latest                    \n9a4df3ed340c    Up 41 minutes                  hbbr                                               rustdesk/rustdesk-server:latest                    \nfa1215b4be74    Up 12 hours                    firefly                                            uusec/firefly:latest\n''';\n    final lines = raw.split('\\n');\n    const ids = ['0e9e2ef860d2', '9a4df3ed340c', 'fa1215b4be74'];\n    const names = ['hbbs', 'hbbr', 'firefly'];\n    const images = [\n      'rustdesk/rustdesk-server:latest',\n      'rustdesk/rustdesk-server:latest',\n      'uusec/firefly:latest',\n    ];\n    const states = ['Up 2 hours', 'Up 41 minutes', 'Up 12 hours'];\n    for (var idx = 1; idx < lines.length; idx++) {\n      final raw = lines[idx];\n      if (raw.isEmpty) continue;\n      final ps = DockerPs.parse(raw);\n      expect(ps.id, ids[idx - 1]);\n      expect(ps.names, names[idx - 1]);\n      expect(ps.image, images[idx - 1]);\n      expect(ps.state, states[idx - 1]);\n      expect(ps.status, ContainerStatus.running);\n      expect(ps.status.isRunning, true);\n    }\n  });\n\n  test('docker ps status detection', () {\n    // Test various Docker container states\n    final testCases = [\n      // Running states\n      {'state': 'Up 2 minutes', 'status': ContainerStatus.running},\n      {'state': 'Up 1 hour', 'status': ContainerStatus.running},\n      {'state': 'UP 30 seconds', 'status': ContainerStatus.running}, // Case insensitive\n      {'state': 'up 5 days', 'status': ContainerStatus.running}, // Case insensitive\n      \n      // Non-running states\n      {'state': 'Exited (0) 5 minutes ago', 'status': ContainerStatus.exited},\n      {'state': 'Created', 'status': ContainerStatus.created},\n      {'state': 'Paused', 'status': ContainerStatus.paused},\n      {'state': 'Restarting', 'status': ContainerStatus.restarting},\n      {'state': 'Removing', 'status': ContainerStatus.removing},\n      {'state': 'Dead', 'status': ContainerStatus.dead},\n      \n      // Edge cases\n      {'state': null, 'status': ContainerStatus.unknown},\n      {'state': '', 'status': ContainerStatus.unknown},\n      {'state': 'Some Unknown Status', 'status': ContainerStatus.unknown},\n    ];\n\n    for (final testCase in testCases) {\n      final ps = DockerPs(id: 'test', state: testCase['state'] as String?);\n      final expectedStatus = testCase['status'] as ContainerStatus;\n      expect(\n        ps.status, \n        expectedStatus, \n        reason: 'State \"${testCase['state']}\" should be ${expectedStatus.name}'\n      );\n      \n      // Test status.isRunning method\n      expect(\n        ps.status.isRunning,\n        expectedStatus.isRunning,\n        reason: 'State \"${testCase['state']}\" isRunning should match status.isRunning'\n      );\n    }\n  });\n\n  test('podman ps status detection', () {\n    final testCases = [\n      {'exited': false, 'status': ContainerStatus.running},\n      {'exited': true, 'status': ContainerStatus.exited},\n      {'exited': null, 'status': ContainerStatus.unknown},\n    ];\n\n    for (final testCase in testCases) {\n      final ps = PodmanPs(id: 'test', exited: testCase['exited'] as bool?);\n      final expectedStatus = testCase['status'] as ContainerStatus;\n      expect(\n        ps.status,\n        expectedStatus,\n        reason: 'Exited \"${testCase['exited']}\" should be ${expectedStatus.name}'\n      );\n      \n      // Test status.isRunning method\n      expect(\n        ps.status.isRunning,\n        expectedStatus.isRunning,\n        reason: 'Exited \"${testCase['exited']}\" isRunning should match status.isRunning'\n      );\n    }\n  });\n\n  test('container status utility methods', () {\n    expect(ContainerStatus.running.isRunning, true);\n    expect(ContainerStatus.exited.isRunning, false);\n    expect(ContainerStatus.created.isRunning, false);\n    \n    expect(ContainerStatus.exited.canStart, true);\n    expect(ContainerStatus.created.canStart, true);\n    expect(ContainerStatus.running.canStart, false);\n    \n    expect(ContainerStatus.running.canStop, true);\n    expect(ContainerStatus.paused.canStop, true);\n    expect(ContainerStatus.exited.canStop, false);\n    \n    expect(ContainerStatus.running.canRestart, true);\n    expect(ContainerStatus.removing.canRestart, false);\n    expect(ContainerStatus.unknown.canRestart, false);\n  });\n}\n"
  },
  {
    "path": "test/core_utils_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/core/utils/comparator.dart';\n\nvoid main() {\n  group('ChainComparator Tests', () {\n    test('comparing sorts correctly', () {\n      final list = ['b', 'c', 'a'];\n      list.sort(ChainComparator.comparing<String, String>((s) => s).call);\n      expect(list, equals(['a', 'b', 'c']));\n    });\n\n    test('thenCompareBy sorts correctly', () {\n      final list = [('b', '2'), ('a', '3'), ('b', '1')];\n      list.sort(\n        ChainComparator.comparing<(String, String), String>((t) => t.$1)\n            .thenCompareBy<String>((t) => t.$2, reversed: false).call,\n      );\n      expect(list, equals([('a', '3'), ('b', '1'), ('b', '2')]));\n    });\n\n    test('thenCompareBy with reversed sorts correctly', () {\n      final list = [('b', '2'), ('a', '3'), ('b', '1')];\n      list.sort(\n        ChainComparator.comparing<(String, String), String>((t) => t.$1)\n            .thenCompareBy<String>((t) => t.$2, reversed: true).call,\n      );\n      expect(list, equals([('a', '3'), ('b', '2'), ('b', '1')]));\n    });\n\n    test('thenWithComparator sorts correctly', () {\n      final list = ['b', 'c', 'a'];\n      list.sort(ChainComparator.comparing<String, String>((s) => s)\n          .thenWithComparator((a, b) => a.length.compareTo(b.length)).call);\n      expect(list, equals(['a', 'b', 'c']));\n    });\n\n    test('thenCompareByReversed sorts correctly', () {\n      final list = [('b', '2'), ('a', '3'), ('b', '1')];\n      list.sort(\n        ChainComparator.comparing<(String, String), String>((t) => t.$1)\n            .thenCompareByReversed<String>((t) => t.$2).call,\n      );\n      expect(list, equals([('a', '3'), ('b', '2'), ('b', '1')]));\n    });\n\n    test('thenTrueFirst sorts correctly', () {\n      final list = [('a', false), ('b', true), ('c', false)];\n      list.sort(\n        ChainComparator.empty().thenTrueFirst((t) => t.$2).thenWithComparator((a, b) => a.$1.compareTo(b.$1)).call,\n      );\n      expect(list, equals([('b', true), ('a', false), ('c', false)]));\n    });\n\n    test('reversed sorts correctly', () {\n      final list = ['a', 'c', 'b'];\n      final comparator = ChainComparator.comparing<String, String>((s) => s);\n      list.sort(comparator.reversed().call);\n      expect(list, equals(['c', 'b', 'a']));\n    });\n  });\n\n  group('Comparators Tests', () {\n    test('compareStringCaseInsensitive sorts correctly', () {\n      final list = ['b', 'C', 'a'];\n      list.sort(Comparators.compareStringCaseInsensitive());\n      expect(list, equals(['a', 'b', 'C']));\n    });\n\n    test('compareStringCaseInsensitive with uppercaseFirst sorts correctly', () {\n      final list = ['b', 'C', 'a', 'B'];\n      list.sort(Comparators.compareStringCaseInsensitive(uppercaseFirst: true));\n      expect(list, equals(['a', 'B', 'b', 'C']));\n    });\n  });\n}\n"
  },
  {
    "path": "test/cpu_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/cpu.dart';\n\nvoid main() {\n  group('CPU Model Tests', () {\n    test('Test SingleCpuCore.parse', () {\n      const raw = 'cpu  18232538 52837 5772391 334460731 247294 0 134107 0 0 0';\n\n      final result = SingleCpuCore.parse(raw);\n      expect(result.length, 1);\n      expect(result[0].id, 'cpu');\n      expect(result[0].total, 358899898);\n    });\n    test('Test Cpus calculation', () {\n      final pre = SingleCpuCore.parse(\n          'cpu 18232538 52837 5772391 334460731 247294 0 134107 0 0 0');\n      final now = SingleCpuCore.parse(\n          'cpu 18232638 52937 5772491 334460831 247294 0 134107 0 0 0');\n      final cpus = Cpus(pre, now);\n      cpus.onUpdate();\n      expect(cpus.usedPercent(), closeTo(75.0, 0.1));\n      expect(cpus.user, closeTo(25.0, 0.1));\n      expect(cpus.sys, closeTo(25.0, 0.1));\n    });\n\n    test('Test parseBsdCpu for macOS', () {\n      const raw = 'CPU usage: 14.70% user, 12.76% sys, 72.52% idle';\n      final cpus = parseBsdCpu(raw);\n      final status = cpus.now.first;\n      expect(status.user, 14);\n      expect(status.sys, 12);\n      expect(status.idle, 72);\n    });\n\n    test('Test parseBsdCpu for FreeBSD', () {\n      const raw =\n          'CPU: 5.2% user, 0.0% nice, 3.1% system, 0.1% interrupt, 91.6% idle';\n      final cpus = parseBsdCpu(raw);\n      final status = cpus.now.first;\n      expect(status.user, 5);\n      expect(status.nice, 0);\n      expect(status.sys, 3);\n      expect(status.irq, 0);\n      expect(status.idle, 91);\n    });\n  });\n}\n"
  },
  {
    "path": "test/disabled_cmd_types_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/app/scripts/cmd_types.dart';\nimport 'package:server_box/data/model/app/scripts/shell_func.dart';\nimport 'package:server_box/data/model/server/system.dart';\n\nvoid main() {\n  group('disabledCmdTypes filtering', () {\n    test('filters Linux status commands when disabled', () {\n      final disabled = <String>{\n        StatusCmdType.net.displayName,\n        StatusCmdType.sys.displayName,\n      }.toList();\n\n      final script = ShellFuncManager.allScript(\n        null,\n        systemType: SystemType.linux,\n        disabledCmdTypes: disabled,\n      );\n\n      // Linux-specific commands should be removed\n      expect(script, isNot(contains('cat /proc/net/dev'))); // net\n      expect(script, isNot(contains('cat /etc/*-release | grep ^PRETTY_NAME'))); // sys\n\n      // Other commands should remain\n      expect(script, contains('uptime'));\n      expect(script, contains('date +%s'));\n    });\n\n    test('filters BSD status commands when disabled', () {\n      final disabled = <String>{\n        BSDStatusCmdType.sys.displayName,\n        BSDStatusCmdType.mem.displayName,\n      }.toList();\n\n      final script = ShellFuncManager.allScript(\n        null,\n        systemType: SystemType.linux, // Unix builder is used for Linux/BSD\n        disabledCmdTypes: disabled,\n      );\n\n      // BSD-specific commands should be removed\n      expect(script, isNot(contains('uname -or'))); // sys\n      expect(script, isNot(contains('top -l 1 | grep PhysMem'))); // mem\n\n      // Linux equivalents should remain\n      expect(script, contains('cat /etc/*-release | grep ^PRETTY_NAME'));\n      expect(script, contains(\"cat /proc/meminfo | grep -E 'Mem|Swap'\"));\n    });\n\n    test('filters Windows status commands when disabled', () {\n      final disabled = <String>{\n        WindowsStatusCmdType.net.displayName,\n        WindowsStatusCmdType.uptime.displayName,\n        WindowsStatusCmdType.temp.displayName,\n      }.toList();\n\n      final script = ShellFuncManager.allScript(\n        null,\n        systemType: SystemType.windows,\n        disabledCmdTypes: disabled,\n      );\n\n      // Windows-specific commands should be removed\n      expect(script, isNot(contains('LastBootUpTime'))); // uptime\n      expect(script, isNot(contains('MSAcpi_ThermalZoneTemperature'))); // temp\n\n      // Other Windows commands should remain\n      expect(script, contains('Get-Process'));\n      expect(script, contains('Get-WmiObject -Class Win32_OperatingSystem'));\n    });\n\n    test('ignores disabled names for other platforms', () {\n      final disabled = <String>{\n        WindowsStatusCmdType.sys.displayName,\n        WindowsStatusCmdType.net.displayName,\n      }.toList();\n\n      final script = ShellFuncManager.allScript(\n        null,\n        systemType: SystemType.linux,\n        disabledCmdTypes: disabled,\n      );\n\n      // Linux commands should not be affected by Windows-only disables\n      expect(script, contains('cat /etc/*-release | grep ^PRETTY_NAME'));\n      expect(script, contains('cat /proc/net/dev'));\n    });\n\n    test('disabling all status commands removes separators', () {\n      final allUnixDisabled = <String>{\n        ...StatusCmdType.values.map((e) => e.displayName),\n        ...BSDStatusCmdType.values.map((e) => e.displayName),\n      }.toList();\n\n      final unixScript = ShellFuncManager.allScript(\n        null,\n        systemType: SystemType.linux,\n        disabledCmdTypes: allUnixDisabled,\n      );\n\n      // No status separators for Unix script\n      expect(unixScript, isNot(contains('SrvBoxSep.')));\n\n      final allWinDisabled = <String>{\n        ...WindowsStatusCmdType.values.map((e) => e.displayName),\n      }.toList();\n\n      final windowsScript = ShellFuncManager.allScript(\n        null,\n        systemType: SystemType.windows,\n        disabledCmdTypes: allWinDisabled,\n      );\n\n      // No status separators for Windows script\n      expect(windowsScript, isNot(contains('SrvBoxSep.')));\n    });\n  });\n}\n\n"
  },
  {
    "path": "test/disk_smart_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/disk_smart.dart';\n\nconst _raw = '''\n{\n  \"json_format_version\": [\n    1,\n    0\n  ],\n  \"smartctl\": {\n    \"version\": [\n      7,\n      4\n    ],\n    \"pre_release\": false,\n    \"svn_revision\": \"5530\",\n    \"platform_info\": \"x86_64-linux-6.6.58-rt45-intel-ese-standard-lts-rt\",\n    \"build_info\": \"(local build)\",\n    \"argv\": [\n      \"smartctl\",\n      \"-A\",\n      \"-j\",\n      \"/dev/sda\"\n    ],\n    \"drive_database_version\": {\n      \"string\": \"7.3/5528\"\n    },\n    \"exit_status\": 0\n  },\n  \"local_time\": {\n    \"time_t\": 1749074092,\n    \"asctime\": \"Thu Jun  5 05:54:52 2025 CST\"\n  },\n  \"device\": {\n    \"name\": \"/dev/sda\",\n    \"info_name\": \"/dev/sda [SAT]\",\n    \"type\": \"sat\",\n    \"protocol\": \"ATA\"\n  },\n  \"ata_smart_attributes\": {\n    \"revision\": 16,\n    \"table\": [\n      {\n        \"id\": 9,\n        \"name\": \"Power_On_Hours\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 18,\n          \"string\": \"-O--C- \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 17472,\n          \"string\": \"17472\"\n        }\n      },\n      {\n        \"id\": 12,\n        \"name\": \"Power_Cycle_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 18,\n          \"string\": \"-O--C- \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 1948,\n          \"string\": \"1948\"\n        }\n      },\n      {\n        \"id\": 167,\n        \"name\": \"Write_Protect_Mode\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 34,\n          \"string\": \"-O---K \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": false,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 168,\n        \"name\": \"SATA_Phy_Error_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 18,\n          \"string\": \"-O--C- \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 172,\n        \"name\": \"Erase_Fail_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 173,\n        \"name\": \"MaxAvgErase_Ct\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 0,\n          \"string\": \"------ \",\n          \"prefailure\": false,\n          \"updated_online\": false,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": false,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 8257696,\n          \"string\": \"160 (Average 126)\"\n        }\n      },\n      {\n        \"id\": 181,\n        \"name\": \"Program_Fail_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 18,\n          \"string\": \"-O--C- \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 187,\n        \"name\": \"Reported_Uncorrect\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 192,\n        \"name\": \"Unsafe_Shutdown_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 18,\n          \"string\": \"-O--C- \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 141,\n          \"string\": \"141\"\n        }\n      },\n      {\n        \"id\": 194,\n        \"name\": \"Temperature_Celsius\",\n        \"value\": 65,\n        \"worst\": 39,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 35,\n          \"string\": \"PO---K \",\n          \"prefailure\": true,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": false,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 261993922595,\n          \"string\": \"35 (Min/Max 14/61)\"\n        }\n      },\n      {\n        \"id\": 196,\n        \"name\": \"Reallocated_Event_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 0,\n          \"string\": \"------ \",\n          \"prefailure\": false,\n          \"updated_online\": false,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": false,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 218,\n        \"name\": \"CRC_Error_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 0,\n          \"string\": \"------ \",\n          \"prefailure\": false,\n          \"updated_online\": false,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": false,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 231,\n        \"name\": \"SSD_Life_Left\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 19,\n          \"string\": \"PO--C- \",\n          \"prefailure\": true,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 93,\n          \"string\": \"93\"\n        }\n      },\n      {\n        \"id\": 233,\n        \"name\": \"Flash_Writes_GiB\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 19,\n          \"string\": \"PO--C- \",\n          \"prefailure\": true,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 17618,\n          \"string\": \"17618\"\n        }\n      },\n      {\n        \"id\": 241,\n        \"name\": \"Lifetime_Writes_GiB\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 18,\n          \"string\": \"-O--C- \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 11520,\n          \"string\": \"11520\"\n        }\n      },\n      {\n        \"id\": 242,\n        \"name\": \"Lifetime_Reads_GiB\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 18,\n          \"string\": \"-O--C- \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 12361,\n          \"string\": \"12361\"\n        }\n      },\n      {\n        \"id\": 244,\n        \"name\": \"Average_Erase_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 0,\n          \"string\": \"------ \",\n          \"prefailure\": false,\n          \"updated_online\": false,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": false,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 126,\n          \"string\": \"126\"\n        }\n      },\n      {\n        \"id\": 245,\n        \"name\": \"Max_Erase_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 0,\n          \"string\": \"------ \",\n          \"prefailure\": false,\n          \"updated_online\": false,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": false,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 160,\n          \"string\": \"160\"\n        }\n      },\n      {\n        \"id\": 246,\n        \"name\": \"Total_Erase_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 0,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 0,\n          \"string\": \"------ \",\n          \"prefailure\": false,\n          \"updated_online\": false,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": false,\n          \"auto_keep\": false\n        },\n        \"raw\": {\n          \"value\": 2749648,\n          \"string\": \"2749648\"\n        }\n      }\n    ]\n  },\n  \"power_on_time\": {\n    \"hours\": 17472\n  },\n  \"power_cycle_count\": 1948,\n  \"temperature\": {\n    \"current\": 35\n  }\n}''';\n\nvoid main() {\n  group('DiskSmart', () {\n    late DiskSmart diskSmart;\n\n    setUp(() {\n      final parsedResults = DiskSmart.parse(_raw);\n      expect(parsedResults.length, 1, reason: 'Should parse one disk entry');\n      diskSmart = parsedResults.first;\n    });\n\n    test('parses basic device info correctly', () {\n      expect(diskSmart.device, '/dev/sda');\n      expect(diskSmart.temperature, 35);\n      expect(diskSmart.powerOnHours, 17472);\n      expect(diskSmart.powerCycleCount, 1948);\n    });\n\n    test('has correct SMART attributes', () {\n      expect(diskSmart.smartAttributes.length, isNot(0));\n      final tempAttr = diskSmart.getAttribute('Temperature_Celsius');\n      expect(tempAttr, isNotNull);\n      expect(tempAttr?.value, 65);\n      expect(tempAttr?.worst, 39);\n      expect(tempAttr?.rawString, '35 (Min/Max 14/61)');\n\n      final powerOnAttr = diskSmart.getAttribute('Power_On_Hours');\n      expect(powerOnAttr?.rawValue, 17472);\n\n      // Test non-existent attribute\n      expect(diskSmart.getAttribute('NonExistent'), isNull);\n    });\n\n    test('extracts attribute flags correctly', () {\n      final tempAttr = diskSmart.getAttribute('Temperature_Celsius');\n      expect(tempAttr?.flags.prefailure, isTrue);\n      expect(tempAttr?.flags.updatedOnline, isTrue);\n      expect(tempAttr?.flags.performance, isFalse);\n\n      final lifeLeftAttr = diskSmart.getAttribute('SSD_Life_Left');\n      expect(lifeLeftAttr?.flags.prefailure, isTrue);\n      expect(lifeLeftAttr?.flags.eventCount, isTrue);\n    });\n\n    test('calculates SSD health metrics correctly', () {\n      expect(diskSmart.ssdLifeLeft, 93);\n      expect(diskSmart.lifetimeWritesGiB, 11520);\n      expect(diskSmart.lifetimeReadsGiB, 12361);\n      expect(diskSmart.unsafeShutdownCount, 141);\n      expect(diskSmart.averageEraseCount, 126);\n      expect(diskSmart.maxEraseCount, 160);\n    });\n\n    test('toMap() converts all important data', () {\n      final map = diskSmart.toJson();\n      expect(map['device'], '/dev/sda');\n      expect(map['temperature'], 35);\n      expect(map['powerOnHours'], 17472);\n      expect(map['powerCycleCount'], 1948);\n      expect(map['smartAttributes'], isA<Map>());\n    });\n  });\n\n  group('DiskSmart parsing edge cases', () {\n    test('handles empty input', () {\n      final results = DiskSmart.parse('');\n      expect(results, isEmpty);\n    });\n\n    test('handles malformed JSON gracefully', () {\n      final results = DiskSmart.parse('{not valid json}');\n      expect(results, isEmpty);\n    });\n\n    test('handles multiple disk data', () {\n      final results = DiskSmart.parse('$_raw\\n\\n$_raw');\n      expect(results.length, 2);\n    });\n  });\n}\n"
  },
  {
    "path": "test/disk_test.dart",
    "content": "// ignore_for_file: avoid_print\n\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/disk.dart';\n\nvoid main() {\n  group('Disk parsing', () {\n    test('parse traditional df output', () {\n      for (final raw in _raws) {\n        final disks = Disk.parse(raw);\n        expect(disks, isNotEmpty);\n      }\n    });\n\n    test('parse lsblk JSON output', () {\n      final disks = Disk.parse(_jsonLsblkOutput);\n      expect(disks, isNotEmpty);\n      expect(disks.length, 6); // Should find ext4 root, vfat efi, and ext2 boot\n\n      // Verify root filesystem\n      final rootFs = disks.firstWhere((disk) => disk.mount == '/');\n      expect(rootFs.fsTyp, 'ext4');\n      expect(rootFs.size, BigInt.parse('982141468672') ~/ BigInt.from(1024));\n      expect(rootFs.used, BigInt.parse('552718364672') ~/ BigInt.from(1024));\n      expect(rootFs.avail, BigInt.parse('379457622016') ~/ BigInt.from(1024));\n      expect(rootFs.usedPercent, 56);\n\n      // Verify boot/efi filesystem\n      final efiFs = disks.firstWhere((disk) => disk.mount == '/boot/efi');\n      expect(efiFs.fsTyp, 'vfat');\n      expect(efiFs.size, BigInt.parse('535805952') ~/ BigInt.from(1024));\n      expect(efiFs.usedPercent, 1);\n\n      // Verify boot filesystem\n      final bootFs = disks.firstWhere((disk) => disk.mount == '/boot');\n      expect(bootFs.fsTyp, 'ext2');\n      expect(bootFs.usedPercent, 34);\n    });\n\n    test('parse nested lsblk JSON output with parent/child relationships', () {\n      final disks = Disk.parse(_nestedJsonLsblkOutput);\n      expect(disks, isNotEmpty);\n\n      // Check parent device with children\n      final parentDisk = disks.firstWhere((disk) => disk.path == '/dev/nvme0n1');\n      expect(parentDisk.children, isNotEmpty);\n      expect(parentDisk.children.length, 3);\n\n      // Check one of the children\n      final rootPartition = parentDisk.children.firstWhere((disk) => disk.mount == '/');\n      expect(rootPartition.fsTyp, 'ext4');\n      expect(rootPartition.path, '/dev/nvme0n1p2');\n      expect(rootPartition.usedPercent, 45);\n\n      // Verify we have a child partition with UUID\n      final bootPartition = parentDisk.children.firstWhere((disk) => disk.mount == '/boot');\n      expect(bootPartition.uuid, '12345678-abcd-1234-abcd-1234567890ab');\n    });\n\n    test('DiskUsage handles zero size correctly', () {\n      final usage = DiskUsage(used: BigInt.from(1000), size: BigInt.zero);\n      expect(usage.usedPercent, 0); // Should return 0 instead of throwing\n    });\n\n    test('DiskUsage handles null kname', () {\n      final disks = [\n        Disk(\n          path: '/dev/sda1',\n          mount: '/mnt',\n          usedPercent: 50,\n          used: BigInt.from(5000),\n          size: BigInt.from(10000),\n          avail: BigInt.from(5000),\n          kname: null, // Explicitly null kname\n        ),\n      ];\n\n      final usage = DiskUsage.parse(disks);\n      expect(usage.used, BigInt.from(5000));\n      expect(usage.size, BigInt.from(10000));\n      expect(usage.usedPercent, 50);\n      // This would use the \"unknown\" fallback for kname\n    });\n\n    test('parse df -k output (fallback mode)', () {\n      final disks = Disk.parse(_dfOutput);\n      expect(disks, isNotEmpty);\n      expect(disks.length, 3); // Should find 3 valid filesystems: udev, /dev/vda3, /dev/vda2\n\n      // Verify root filesystem\n      final rootFs = disks.firstWhere((disk) => disk.mount == '/');\n      expect(rootFs.path, '/dev/vda3');\n      expect(rootFs.usedPercent, 47);\n      expect(rootFs.size, BigInt.from(40910528 ~/ 1024)); // df -k output divided by 1024 = MB\n      expect(rootFs.used, BigInt.from(18067948 ~/ 1024));\n      expect(rootFs.avail, BigInt.from(20951380 ~/ 1024));\n\n      // Verify boot/efi filesystem\n      final efiFs = disks.firstWhere((disk) => disk.mount == '/boot/efi');\n      expect(efiFs.path, '/dev/vda2');\n      expect(efiFs.usedPercent, 7);\n      expect(efiFs.size, BigInt.from(192559 ~/ 1024));\n      \n      // Verify udev filesystem is included (virtual filesystem)\n      final udevFs = disks.firstWhere((disk) => disk.path == 'udev');\n      expect(udevFs.mount, '/dev');\n      expect(udevFs.usedPercent, 0);\n      expect(udevFs.size, BigInt.from(864088 ~/ 1024));\n    });\n\n    test('handle empty input gracefully', () {\n      final disks = Disk.parse('');\n      expect(disks, isEmpty);\n    });\n\n    test('handle whitespace-only input', () {\n      final disks = Disk.parse('   \\n\\t  \\r\\n  ');\n      expect(disks, isEmpty);\n    });\n\n    test('handle JSON with null filesystem fields', () {\n      final disks = Disk.parse(_jsonWithNullFields);\n      expect(disks, isNotEmpty);\n      \n      // Should handle null filesystem fields gracefully\n      final disk = disks.firstWhere((disk) => disk.mount == '/');\n      expect(disk.size, BigInt.zero);\n      expect(disk.used, BigInt.zero);\n      expect(disk.avail, BigInt.zero);\n      expect(disk.usedPercent, 0);\n    });\n\n    test('handle JSON with string \"null\" values', () {\n      final disks = Disk.parse(_jsonWithStringNulls);\n      expect(disks, isNotEmpty);\n      \n      // Should handle string \"null\" filesystem fields gracefully\n      final disk = disks.firstWhere((disk) => disk.mount == '/');\n      expect(disk.size, BigInt.zero);\n      expect(disk.used, BigInt.zero);\n      expect(disk.avail, BigInt.zero);\n      expect(disk.usedPercent, 0);\n    });\n\n    test('handle JSON with empty string values', () {\n      final disks = Disk.parse(_jsonWithEmptyStrings);\n      expect(disks, isNotEmpty);\n      \n      // Should handle empty string filesystem fields gracefully\n      final disk = disks.firstWhere((disk) => disk.mount == '/');\n      expect(disk.size, BigInt.zero);\n      expect(disk.used, BigInt.zero);\n      expect(disk.avail, BigInt.zero);\n      expect(disk.usedPercent, 0);\n    });\n\n    test('handle JSON with invalid percentage format', () {\n      final disks = Disk.parse(_jsonWithInvalidPercent);\n      expect(disks, isNotEmpty);\n      \n      // Should handle invalid percentage gracefully\n      final disk = disks.firstWhere((disk) => disk.mount == '/');\n      expect(disk.usedPercent, 0);\n    });\n\n    test('handle JSON with malformed numbers', () {\n      final disks = Disk.parse(_jsonWithMalformedNumbers);\n      expect(disks, isNotEmpty);\n      \n      // Should handle malformed numbers gracefully\n      final disk = disks.firstWhere((disk) => disk.mount == '/');\n      expect(disk.size, BigInt.zero);\n      expect(disk.used, BigInt.zero);\n      expect(disk.avail, BigInt.zero);\n    });\n\n    test('handle JSON parsing errors gracefully', () {\n      final disks = Disk.parse(_malformedJson);\n      expect(disks, isEmpty); // Should fallback to legacy method, which also fails\n    });\n\n    test('handle df output with missing fields', () {\n      final disks = Disk.parse(_dfWithMissingFields);\n      expect(disks, isNotEmpty);\n      \n      // Should handle missing fields gracefully\n      final disk = disks.firstWhere((disk) => disk.mount == '/');\n      expect(disk.usedPercent, 47);\n    });\n\n    test('handle df output with inconsistent formatting', () {\n      final disks = Disk.parse(_dfWithInconsistentFormatting);\n      expect(disks, isNotEmpty);\n      \n      // Should handle inconsistent formatting\n      expect(disks.length, greaterThan(0));\n    });\n\n    test('handle lsblk with success marker', () {\n      final disks = Disk.parse(_lsblkWithSuccessMarker);\n      expect(disks, isNotEmpty);\n      \n      // Should parse JSON and ignore success marker\n      final rootFs = disks.firstWhere((disk) => disk.mount == '/');\n      expect(rootFs.fsTyp, 'ext4');\n      expect(rootFs.usedPercent, 56);\n    });\n\n    test('handle malformed lsblk output fallback', () {\n      final disks = Disk.parse(_malformedLsblkWithDfFallback);\n      expect(disks, isNotEmpty);\n      \n      // Should fallback to df -k parsing when lsblk output is malformed\n      expect(disks.length, 3);\n    });\n  });\n}\n\nconst _jsonLsblkOutput = '''\n{\n   \"blockdevices\": [\n      {\n         \"fstype\": \"LVM2_member\",\n         \"mountpoint\": null,\n         \"fssize\": null,\n         \"fsused\": null,\n         \"fsavail\": null,\n         \"fsuse%\": null\n      },{\n         \"fstype\": \"ext4\",\n         \"mountpoint\": \"/\",\n         \"fssize\": 982141468672,\n         \"fsused\": 552718364672,\n         \"fsavail\": 379457622016,\n         \"fsuse%\": \"56%\"\n      },{\n         \"fstype\": \"swap\",\n         \"mountpoint\": \"[SWAP]\",\n         \"fssize\": null,\n         \"fsused\": null,\n         \"fsavail\": null,\n         \"fsuse%\": null\n      },{\n         \"fstype\": null,\n         \"mountpoint\": null,\n         \"fssize\": null,\n         \"fsused\": null,\n         \"fsavail\": null,\n         \"fsuse%\": null\n      },{\n         \"fstype\": \"vfat\",\n         \"mountpoint\": \"/boot/efi\",\n         \"fssize\": 535805952,\n         \"fsused\": 6127616,\n         \"fsavail\": 529678336,\n         \"fsuse%\": \"1%\"\n      },{\n         \"fstype\": \"ext2\",\n         \"mountpoint\": \"/boot\",\n         \"fssize\": 477210624,\n         \"fsused\": 161541120,\n         \"fsavail\": 290084864,\n         \"fsuse%\": \"34%\"\n      },{\n         \"fstype\": \"crypto_LUKS\",\n         \"mountpoint\": null,\n         \"fssize\": null,\n         \"fsused\": null,\n         \"fsavail\": null,\n         \"fsuse%\": null\n      }\n   ]\n}\n''';\n\nconst _nestedJsonLsblkOutput = '''\n{\n  \"blockdevices\": [\n    {\n      \"name\": \"nvme0n1\",\n      \"kname\": \"nvme0n1\",\n      \"path\": \"/dev/nvme0n1\",\n      \"fstype\": null,\n      \"mountpoint\": null,\n      \"fssize\": null,\n      \"fsused\": null,\n      \"fsavail\": null,\n      \"fsuse%\": null,\n      \"children\": [\n        {\n          \"name\": \"nvme0n1p1\",\n          \"kname\": \"nvme0n1p1\",\n          \"path\": \"/dev/nvme0n1p1\",\n          \"fstype\": \"vfat\",\n          \"mountpoint\": \"/boot/efi\",\n          \"fssize\": \"512000000\",\n          \"fsused\": \"25600000\",\n          \"fsavail\": \"486400000\",\n          \"fsuse%\": \"5%\",\n          \"uuid\": \"98765432-dcba-4321-dcba-0987654321fe\"\n        },\n        {\n          \"name\": \"nvme0n1p2\",\n          \"kname\": \"nvme0n1p2\",\n          \"path\": \"/dev/nvme0n1p2\",\n          \"fstype\": \"ext4\",\n          \"mountpoint\": \"/\",\n          \"fssize\": \"500000000000\",\n          \"fsused\": \"225000000000\",\n          \"fsavail\": \"275000000000\",\n          \"fsuse%\": \"45%\",\n          \"uuid\": \"abcdef12-3456-7890-abcd-ef1234567890\"\n        },\n        {\n          \"name\": \"nvme0n1p3\",\n          \"kname\": \"nvme0n1p3\",\n          \"path\": \"/dev/nvme0n1p3\",\n          \"fstype\": \"ext4\",\n          \"mountpoint\": \"/boot\",\n          \"fssize\": \"1000000000\",\n          \"fsused\": \"500000000\",\n          \"fsavail\": \"500000000\",\n          \"fsuse%\": \"50%\",\n          \"uuid\": \"12345678-abcd-1234-abcd-1234567890ab\"\n        }\n      ]\n    }\n  ]\n}\n''';\n\nconst _raws = [\n  //   '''\n  // Filesystem     1K-blocks     Used Available Use% Mounted on\n  // udev              864088        0    864088   0% /dev\n  // tmpfs             176724      688    176036   1% /run\n  // /dev/vda3       40910528 18067948  20951380  47% /\n  // tmpfs             883612        0    883612   0% /dev/shm\n  // tmpfs               5120        0      5120   0% /run/lock\n  // /dev/vda2         192559    11807    180752   7% /boot/efi\n  // tmpfs             176720      104    176616   1% /run/user/1000\n  // ''',\n  '''\nFilesystem                                                   1K-blocks        Used   Available Use% Mounted on\nudev                                                          16181648           0    16181648   0% /dev\ntmpfs                                                          3270528       37268     3233260   2% /run\nboot-pool/ROOT/23.10.2                                       467090304     3083008   464007296   1% /\ntmpfs                                                         16352624         112    16352512   1% /dev/shm\ntmpfs                                                           102400           0      102400   0% /run/lock\ntmpfs                                                         16352624          12    16352612   1% /tmp\nboot-pool/grub                                               464015744        8448   464007296   1% /boot/grub\nv2000pro                                                    1906569472         128  1906569344   1% /mnt/v2000pro\nv2000pro/local-app                                          1921857408    15288064  1906569344   1% /mnt/v2000pro/local-app\nv2000pro/ix-applications                                    1906569472         128  1906569344   1% /mnt/v2000pro/ix-applications\nv2000pro/sdd                                                1906569472         128  1906569344   1% /mnt/v2000pro/sdd\nv2000pro/ix-applications/catalogs                           1907647744     1078400  1906569344   1% /mnt/v2000pro/ix-applications/catalogs\nv2000pro/ix-applications/k3s                                1907116416      547072  1906569344   1% /mnt/v2000pro/ix-applications/k3s\nv2000pro/ix-applications/releases                           1906569472         128  1906569344   1% /mnt/v2000pro/ix-applications/releases\nv2000pro/ix-applications/default_volumes                    1906569472         128  1906569344   1% /mnt/v2000pro/ix-applications/default_volumes\nwd16-raidz                                                 19217443072         256 19217442816   1% /mnt/wd16-raidz\nwd16-raidz/games                                           19386510592   169067776 19217442816   1% /mnt/wd16-raidz/games\nwd16-raidz/store                                           20526360832  1308918016 19217442816   7% /mnt/wd16-raidz/store\nwd16-raidz/share                                              62914560    24397312    38517248  39% /mnt/wd16-raidz/share\nwd16-raidz/media                                           60509795968 41292353152 19217442816  69% /mnt/wd16-raidz/media\nboot-pool/.system                                            464007424         128   464007296   1% /var/db/system\nboot-pool/.system/cores                                        1048576         128     1048448   1% /var/db/system/cores\nboot-pool/.system/samba4                                     464007680         384   464007296   1% /var/db/system/samba4\nboot-pool/.system/rrd-ae32c386e13840b2bf9c0083275e7941       464007424         128   464007296   1% /var/db/system/rrd-ae32c386e13840b2bf9c0083275e7941\nboot-pool/.system/configs-ae32c386e13840b2bf9c0083275e7941   464010368        3072   464007296   1% /var/db/system/configs-ae32c386e13840b2bf9c0083275e7941\nboot-pool/.system/webui                                      464007424         128   464007296   1% /var/db/system/webui\nboot-pool/.system/services                                   464007424         128   464007296   1% /var/db/system/services\nboot-pool/.system/glusterd                                   464007424         128   464007296   1% /var/db/system/glusterd\nboot-pool/.system/ctdb_shared_vol                            464007424         128   464007296   1% /var/db/system/ctdb_shared_vol\nboot-pool/.system/netdata-ae32c386e13840b2bf9c0083275e7941   464239744      232448   464007296   1% /var/db/system/netdata-ae32c386e13840b2bf9c0083275e7941\nv2000pro/ix-applications/k3s/kubelet                        1906569728         384  1906569344   1% /var/lib/kubelet\ntmpfs                                                           512000          12      511988   1% /var/lib/kubelet/pods/26ae1c0e-18cf-4b75-b29d-8ce9636a7da3/volumes/kubernetes.io~projected/kube-api-access-9q9bx\nshm                                                              65536           0       65536   0% /run/k3s/containerd/io.containerd.grpc.v1.cri/sandboxes/c66db36fb35c6c881949ceb69b6c9da9b3ad92e305783ea3b77e65de791f61ba/shm\ntmpfs                                                           409600          12      409588   1% /var/lib/kubelet/pods/df8fac94-f5c1-4d3a-8f45-8e455510a2c8/volumes/kubernetes.io~projected/kube-api-access-4vzjm\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/c66db36fb35c6c881949ceb69b6c9da9b3ad92e305783ea3b77e65de791f61ba/rootfs\nshm                                                              65536           0       65536   0% /run/k3s/containerd/io.containerd.grpc.v1.cri/sandboxes/6f49cc72b9702e2713b67128235c9dd5837b41861022a021c9d315426cc352c4/shm\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/6f49cc72b9702e2713b67128235c9dd5837b41861022a021c9d315426cc352c4/rootfs\ntmpfs                                                           174080          12      174068   1% /var/lib/kubelet/pods/4d744e5b-1b98-41bd-8742-8d987f17c4af/volumes/kubernetes.io~projected/kube-api-access-jrzpb\nshm                                                              65536           0       65536   0% /run/k3s/containerd/io.containerd.grpc.v1.cri/sandboxes/ed0264ba5e5beebb1614da00b9fc9ce2f4241173b4ba8f52f83ffb068dc97034/shm\ntmpfs                                                           307200          12      307188   1% /var/lib/kubelet/pods/9423f3b8-712e-4539-b473-fb6e4b44481e/volumes/kubernetes.io~projected/kube-api-access-frxkj\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/ed0264ba5e5beebb1614da00b9fc9ce2f4241173b4ba8f52f83ffb068dc97034/rootfs\ntmpfs                                                           921600          12      921588   1% /var/lib/kubelet/pods/b6e2f566-3d64-4533-835f-1acf24293db8/volumes/kubernetes.io~projected/kube-api-access-v5rql\nshm                                                              65536           0       65536   0% /run/k3s/containerd/io.containerd.grpc.v1.cri/sandboxes/d6a11b1f80e1406ca31c260b4eadf9c7c0e9db17a98eac5d62cbcaf7ea0a013b/shm\nshm                                                              65536           0       65536   0% /run/k3s/containerd/io.containerd.grpc.v1.cri/sandboxes/76401fc48a79755d503ddac81d37ac2e00e0cccbcbd0de5527e3c132a3d93709/shm\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/d6a11b1f80e1406ca31c260b4eadf9c7c0e9db17a98eac5d62cbcaf7ea0a013b/rootfs\ntmpfs                                                           614400          12      614388   1% /var/lib/kubelet/pods/5676e37b-d0a4-4910-b3fd-cf7ec25f201f/volumes/kubernetes.io~projected/kube-api-access-jrm62\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/76401fc48a79755d503ddac81d37ac2e00e0cccbcbd0de5527e3c132a3d93709/rootfs\nshm                                                              65536           0       65536   0% /run/k3s/containerd/io.containerd.grpc.v1.cri/sandboxes/22f66d268f14cedc9d16ff0c31a4babdb4a082e7444f72a745b8f0e6ccc7cec7/shm\ntmpfs                                                           307200          12      307188   1% /var/lib/kubelet/pods/b528d0cb-cf84-4f25-8d5d-eae89f6c853e/volumes/kubernetes.io~projected/kube-api-access-5bgtp\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/22f66d268f14cedc9d16ff0c31a4babdb4a082e7444f72a745b8f0e6ccc7cec7/rootfs\ntmpfs                                                         32705248          12    32705236   1% /var/lib/kubelet/pods/c2070b60-ec0a-4107-97ad-f1eb475ebac3/volumes/kubernetes.io~projected/kube-api-access-scxv6\nshm                                                              65536           0       65536   0% /run/k3s/containerd/io.containerd.grpc.v1.cri/sandboxes/ceb3da406b1d6f38dfab42cc314b69a510947e42867afae6b33d0d8e10e705b5/shm\ntmpfs                                                         32705248          12    32705236   1% /var/lib/kubelet/pods/593d00aa-16bb-4d18-906d-37731ba3f8bf/volumes/kubernetes.io~projected/kube-api-access-x54qh\nshm                                                              65536           0       65536   0% /run/k3s/containerd/io.containerd.grpc.v1.cri/sandboxes/26547c6bfd920ed69d0d95c48a3d890695b0238420e4c54ee56075dc43e03ea5/shm\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/ceb3da406b1d6f38dfab42cc314b69a510947e42867afae6b33d0d8e10e705b5/rootfs\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/2119c0e105c80619f630691263450234456349d3ddce64b77a184491bda56dd2/rootfs\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/26547c6bfd920ed69d0d95c48a3d890695b0238420e4c54ee56075dc43e03ea5/rootfs\ntmpfs                                                         32705248          12    32705236   1% /var/lib/kubelet/pods/eccfba54-48aa-4950-b3b8-e18ad155e16d/volumes/kubernetes.io~projected/kube-api-access-lc9gs\nshm                                                              65536           0       65536   0% /run/k3s/containerd/io.containerd.grpc.v1.cri/sandboxes/dda821508458f2207ebfb53485b33932724c5ede375efebbfc002b511daef1f3/shm\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/dda821508458f2207ebfb53485b33932724c5ede375efebbfc002b511daef1f3/rootfs\nshm                                                              65536           0       65536   0% /run/k3s/containerd/io.containerd.grpc.v1.cri/sandboxes/510d5900ec9d403ba57689325b5560f713704d69c784487d9c3be127316e1db8/shm\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/510d5900ec9d403ba57689325b5560f713704d69c784487d9c3be127316e1db8/rootfs\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/a873561950eb40a58a6dfa3dd7e74a171a31b598cd3edacd930d09a8fede9ea1/rootfs\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/fa65821375d894f5039a763315734ed51befdafaa5215e30d2f8f889e351c1a3/rootfs\noverlay                                                     1907116416      547072  1906569344   1% /run/k3s/containerd/io.containerd.runtime.v2.task/k8s.io/3e463fd96ee1e2ddba7aca7e8cb161b59f33b0a778e5f7d8c1a5939a72a6f4a5/rootfs\nv2000pro/pve                                                1906694784      125440  1906569344   1% /mnt/v2000pro/pve\nv2000pro/download                                           1906569472         128  1906569344   1% /mnt/v2000pro/download''',\n];\n\nconst _dfOutput = '''\nFilesystem     1K-blocks     Used Available Use% Mounted on\nudev              864088        0    864088   0% /dev\ntmpfs             176724      688    176036   1% /run\n/dev/vda3       40910528 18067948  20951380  47% /\ntmpfs             883612        0    883612   0% /dev/shm\ntmpfs               5120        0      5120   0% /run/lock\n/dev/vda2         192559    11807    180752   7% /boot/efi\ntmpfs             176720      104    176616   1% /run/user/1000\n''';\n\n// Test data for edge cases\nconst _jsonWithNullFields = '''\n{\n  \"blockdevices\": [\n    {\n      \"fstype\": \"ext4\",\n      \"mountpoint\": \"/\",\n      \"fssize\": null,\n      \"fsused\": null,\n      \"fsavail\": null,\n      \"fsuse%\": null,\n      \"path\": \"/dev/sda1\"\n    }\n  ]\n}\n''';\n\nconst _jsonWithStringNulls = '''\n{\n  \"blockdevices\": [\n    {\n      \"fstype\": \"ext4\",\n      \"mountpoint\": \"/\",\n      \"fssize\": \"null\",\n      \"fsused\": \"null\",\n      \"fsavail\": \"null\",\n      \"fsuse%\": \"null\",\n      \"path\": \"/dev/sda1\"\n    }\n  ]\n}\n''';\n\nconst _jsonWithEmptyStrings = '''\n{\n  \"blockdevices\": [\n    {\n      \"fstype\": \"ext4\",\n      \"mountpoint\": \"/\",\n      \"fssize\": \"\",\n      \"fsused\": \"\",\n      \"fsavail\": \"\",\n      \"fsuse%\": \"\",\n      \"path\": \"/dev/sda1\"\n    }\n  ]\n}\n''';\n\nconst _jsonWithInvalidPercent = '''\n{\n  \"blockdevices\": [\n    {\n      \"fstype\": \"ext4\",\n      \"mountpoint\": \"/\",\n      \"fssize\": \"1000000\",\n      \"fsused\": \"500000\",\n      \"fsavail\": \"500000\",\n      \"fsuse%\": \"invalid_percent\",\n      \"path\": \"/dev/sda1\"\n    }\n  ]\n}\n''';\n\nconst _jsonWithMalformedNumbers = '''\n{\n  \"blockdevices\": [\n    {\n      \"fstype\": \"ext4\",\n      \"mountpoint\": \"/\",\n      \"fssize\": \"not_a_number\",\n      \"fsused\": \"invalid\",\n      \"fsavail\": \"broken\",\n      \"fsuse%\": \"50%\",\n      \"path\": \"/dev/sda1\"\n    }\n  ]\n}\n''';\n\nconst _malformedJson = '''\n{\n  \"blockdevices\": [\n    {\n      \"fstype\": \"ext4\",\n      \"mountpoint\": \"/\",\n      \"fssize\": \"1000000\",\n      \"fsused\": \"500000\",\n      \"fsavail\": \"500000\",\n      \"fsuse%\": \"50%\",\n      \"path\": \"/dev/sda1\"\n    }\n  ]\n  // Missing closing brace and malformed structure\n''';\n\nconst _dfWithMissingFields = '''\nFilesystem     1K-blocks     Used Available Use% Mounted on\n/dev/vda3       40910528 18067948  20951380  47% /\n''';\n\nconst _dfWithInconsistentFormatting = '''\nFilesystem    1K-blocks    Used    Available   Use%   Mounted on\n/dev/sda1     1000000      500000  500000      50%    /\n/dev/sda2     2000000      1000000 1000000     50%    /home\n   udev       864088       0       864088      0%     /dev\n''';\n\nconst _lsblkWithSuccessMarker = '''\n{\n  \"blockdevices\": [\n    {\n      \"fstype\": \"ext4\",\n      \"mountpoint\": \"/\",\n      \"fssize\": 982141468672,\n      \"fsused\": 552718364672,\n      \"fsavail\": 379457622016,\n      \"fsuse%\": \"56%\",\n      \"path\": \"/dev/sda1\"\n    }\n  ]\n}\nLSBLK_SUCCESS\n''';\n\nconst _malformedLsblkWithDfFallback = '''\nFilesystem     1K-blocks     Used Available Use% Mounted on\nudev              864088        0    864088   0% /dev\ntmpfs             176724      688    176036   1% /run\n/dev/vda3       40910528 18067948  20951380  47% /\ntmpfs             883612        0    883612   0% /dev/shm\ntmpfs               5120        0      5120   0% /run/lock\n/dev/vda2         192559    11807    180752   7% /boot/efi\ntmpfs             176720      104    176616   1% /run/user/1000\n''';\n"
  },
  {
    "path": "test/jump_chain_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/core/utils/jump_chain.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\n\nSpi _spi({required String id, required String name, String? jumpId}) {\n  return Spi(\n    id: id,\n    name: name,\n    ip: '$name.example.com',\n    port: 22,\n    user: 'root',\n    jumpId: jumpId,\n  );\n}\n\nvoid main() {\n  group('JumpChain', () {\n    test('wouldCreateJumpCycle returns false for valid chain', () {\n      final servers = <String, Spi>{\n        'A': _spi(id: 'A', name: 'a'),\n        'B': _spi(id: 'B', name: 'b', jumpId: 'A'),\n      };\n\n      final result = wouldCreateJumpCycle(\n        currentServerId: 'C',\n        candidateJumpId: 'B',\n        serversById: servers,\n      );\n\n      expect(result, isFalse);\n    });\n\n    test('wouldCreateJumpCycle detects cycle back to current server', () {\n      final servers = <String, Spi>{\n        'A': _spi(id: 'A', name: 'a', jumpId: 'B'),\n        'B': _spi(id: 'B', name: 'b', jumpId: 'C'),\n        'C': _spi(id: 'C', name: 'c'),\n      };\n\n      final result = wouldCreateJumpCycle(\n        currentServerId: 'C',\n        candidateJumpId: 'A',\n        serversById: servers,\n      );\n\n      expect(result, isTrue);\n    });\n\n    test('wouldCreateJumpCycle treats existing malformed loop as invalid', () {\n      final servers = <String, Spi>{\n        'A': _spi(id: 'A', name: 'a', jumpId: 'B'),\n        'B': _spi(id: 'B', name: 'b', jumpId: 'A'),\n      };\n\n      final result = wouldCreateJumpCycle(\n        currentServerId: 'C',\n        candidateJumpId: 'A',\n        serversById: servers,\n      );\n\n      expect(result, isTrue);\n    });\n\n    test('wouldCreateJumpCycle validates new server with null current id', () {\n      final servers = <String, Spi>{\n        'A': _spi(id: 'A', name: 'a', jumpId: 'B'),\n        'B': _spi(id: 'B', name: 'b', jumpId: 'A'),\n      };\n\n      final result = wouldCreateJumpCycle(\n        currentServerId: null,\n        candidateJumpId: 'A',\n        serversById: servers,\n      );\n\n      expect(result, isTrue);\n    });\n\n    test('collectJumpServers collects reachable jump servers', () {\n      final target = _spi(id: 'T', name: 'target', jumpId: 'A');\n      final servers = <String, Spi>{\n        'A': _spi(id: 'A', name: 'a', jumpId: 'B'),\n        'B': _spi(id: 'B', name: 'b'),\n      };\n\n      final chain = collectJumpServers(spi: target, serversById: servers);\n\n      expect(chain.keys.toList(), ['A', 'B']);\n    });\n  });\n}\n"
  },
  {
    "path": "test/memory_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/memory.dart';\n\nvoid main() {\n  group('Memory Model Tests', () {\n    test('Test Memory.parse', () {\n      const raw = '''MemTotal:       32768 kB\nMemFree:        16384 kB\nMemAvailable:   24576 kB''';\n      final result = Memory.parse(raw);\n      expect(result.total, 32768);\n      expect(result.free, 16384);\n      expect(result.avail, 24576);\n      expect(result.usedPercent, closeTo(0.25, 0.01));\n      expect(result.availPercent, closeTo(0.75, 0.01));\n    });\n\n    test('Test parseBsdMemory for macOS', () {\n      const raw = 'PhysMem: 32G used (1536M wired), 64G unused.';\n      final result = parseBsdMemory(raw);\n      expect(result.total, (32 + 64) * 1024 * 1024);\n      expect(result.free, 64 * 1024 * 1024);\n      expect(result.avail, 64 * 1024 * 1024);\n    });\n\n    test('Test parseBsdMemory for FreeBSD', () {\n      const raw =\n          'Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free';\n      final result = parseBsdMemory(raw);\n      expect(result.total, (456 + 2918 + 1127 + 187 + 829 + 3535) * 1024);\n      expect(result.free, 3535 * 1024);\n      expect(result.avail, 3535 * 1024);\n    });\n  });\n}\n"
  },
  {
    "path": "test/net_speed_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/net_speed.dart';\n\nvoid main() {\n  group('NetSpeedPart Tests', () {\n    test('NetSpeedPart.same method', () {\n      final part1 = NetSpeedPart('eth0', BigInt.from(1000), BigInt.from(500), 1000);\n      final part2 = NetSpeedPart('eth0', BigInt.from(2000), BigInt.from(1000), 2000);\n      final part3 = NetSpeedPart('eth1', BigInt.from(1000), BigInt.from(500), 1000);\n      \n      expect(part1.same(part2), isTrue);\n      expect(part1.same(part3), isFalse);\n    });\n  });\n\n  group('NetSpeed Tests', () {\n    test('NetSpeed.parse with Linux format', () {\n      const raw = '''\nInter-|   Receive                                                |  Transmit\n face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed\n    lo: 45929941  269112    0    0    0     0          0         0 45929941  269112    0    0    0     0       0          0\n  eth0: 48481023  505772    0    0    0     0          0         0 36002262  202307    0    0    0     0       0          0\n  wlan0: 12345678  123456    0    0    0     0          0         0 87654321  123456    0    0    0     0       0          0\n''';\n\n      final parts = NetSpeed.parse(raw, 1000);\n      expect(parts, hasLength(3));\n      expect(parts[0].device, equals('lo'));\n      expect(parts[0].bytesIn, equals(BigInt.from(45929941)));\n      expect(parts[0].bytesOut, equals(BigInt.from(45929941)));\n      \n      expect(parts[1].device, equals('eth0'));\n      expect(parts[1].bytesIn, equals(BigInt.from(48481023)));\n      expect(parts[1].bytesOut, equals(BigInt.from(36002262)));\n    });\n\n    test('NetSpeed.parseBsd with BSD format', () {\n      const raw = '''\nName       Mtu   Network       Address            Ipkts Ierrs     Ibytes    Opkts Oerrs     Obytes  Coll\nlo0        16384 <Link#1>      -              17296531     0 2524959720 17296531     0 2524959720     0\nen0        1500  <Link#4>    22:20:xx:xx:xx:e6   739447     0  693997876   535600     0   79008877     0\nen1        1500  <Link#5>    88:d8:xx:xx:xx:1d        0     0          0        0     0          0     0\n''';\n\n      final parts = NetSpeed.parseBsd(raw, 1000);\n      expect(parts, hasLength(3));\n      expect(parts[0].device, equals('lo0'));\n      expect(parts[0].bytesIn, equals(BigInt.from(2524959720)));\n      expect(parts[0].bytesOut, equals(BigInt.from(2524959720)));\n      \n      expect(parts[1].device, equals('en0'));\n      expect(parts[1].bytesIn, equals(BigInt.from(693997876)));\n      expect(parts[1].bytesOut, equals(BigInt.from(79008877)));\n      \n      expect(parts[2].device, equals('en1'));\n      expect(parts[2].bytesIn, equals(BigInt.from(0)));\n      expect(parts[2].bytesOut, equals(BigInt.from(0)));\n    });\n\n    test('NetSpeed.parseBsd skips disabled devices', () {\n      const raw = '''\nName       Mtu   Network       Address            Ipkts Ierrs     Ibytes    Opkts Oerrs     Obytes  Coll\nlo0        16384 <Link#1>      -              17296531     0 2524959720 17296531     0 2524959720     0\nen2*       1500  <Link#11>   36:7c:xx:xx:xx:xx        0     0          0        0     0          0     0\nen0        1500  <Link#4>    22:20:xx:xx:xx:e6   739447     0  693997876   535600     0   79008877     0\n''';\n\n      final parts = NetSpeed.parseBsd(raw, 1000);\n      expect(parts, hasLength(2));\n      expect(parts[0].device, equals('lo0'));\n      expect(parts[1].device, equals('en0'));\n    });\n\n    test('NetSpeed speed calculations', () {\n      final oldData = [\n        NetSpeedPart('eth0', BigInt.from(1000000), BigInt.from(500000), 1000),\n        NetSpeedPart('lo', BigInt.from(2000000), BigInt.from(1000000), 1000),\n      ];\n      final newData = [\n        NetSpeedPart('eth0', BigInt.from(2000000), BigInt.from(1000000), 2000),\n        NetSpeedPart('lo', BigInt.from(3000000), BigInt.from(2000000), 2000),\n      ];\n      \n      final netSpeed = NetSpeed(oldData, newData);\n      netSpeed.onUpdate();\n      \n      expect(netSpeed.devices, contains('eth0'));\n      expect(netSpeed.devices, contains('lo'));\n      expect(netSpeed.realIfaces, contains('eth0'));\n      expect(netSpeed.realIfaces, isNot(contains('lo')));\n    });\n\n    test('NetSpeed speed calculations for specific device', () {\n      final oldData = [\n        NetSpeedPart('eth0', BigInt.from(1000000), BigInt.from(500000), 1000),\n      ];\n      final newData = [\n        NetSpeedPart('eth0', BigInt.from(2000000), BigInt.from(1000000), 2000),\n      ];\n      \n      final netSpeed = NetSpeed(oldData, newData);\n      netSpeed.onUpdate();\n      \n      final speedIn = netSpeed.speedIn(device: 'eth0');\n      final speedOut = netSpeed.speedOut(device: 'eth0');\n      final sizeIn = netSpeed.sizeIn(device: 'eth0');\n      final sizeOut = netSpeed.sizeOut(device: 'eth0');\n      \n      expect(speedIn, equals('1000 B/s'));\n      expect(speedOut, equals('500 B/s'));\n      expect(sizeIn, equals('1.9 MB'));\n      expect(sizeOut, equals('976.6 KB'));\n    });\n\n    test('NetSpeed handles empty data gracefully', () {\n      final netSpeed = NetSpeed([], []);\n      netSpeed.onUpdate();\n      \n      expect(netSpeed.speedIn(), equals('N/A'));\n      expect(netSpeed.speedOut(), equals('N/A'));\n      expect(netSpeed.sizeIn(), equals('N/A'));\n      expect(netSpeed.sizeOut(), equals('N/A'));\n    });\n\n    test('NetSpeed real interface filtering', () {\n      final parts = [\n        NetSpeedPart('eth0', BigInt.from(1000), BigInt.from(500), 1000),\n        NetSpeedPart('wlan0', BigInt.from(1000), BigInt.from(500), 1000),\n        NetSpeedPart('en0', BigInt.from(1000), BigInt.from(500), 1000),\n        NetSpeedPart('lo', BigInt.from(1000), BigInt.from(500), 1000),\n        NetSpeedPart('docker0', BigInt.from(1000), BigInt.from(500), 1000),\n      ];\n      \n      final netSpeed = NetSpeed([], parts);\n      netSpeed.onUpdate();\n      \n      expect(netSpeed.realIfaces, contains('eth0'));\n      expect(netSpeed.realIfaces, contains('wlan0'));\n      expect(netSpeed.realIfaces, contains('en0'));\n      expect(netSpeed.realIfaces, isNot(contains('lo')));\n      expect(netSpeed.realIfaces, isNot(contains('docker0')));\n    });\n\n    test('NetSpeed deviceIdx method', () {\n      final parts = [\n        NetSpeedPart('eth0', BigInt.from(1000), BigInt.from(500), 1000),\n        NetSpeedPart('eth1', BigInt.from(1000), BigInt.from(500), 1000),\n      ];\n      \n      final netSpeed = NetSpeed([], parts);\n      netSpeed.onUpdate();\n      \n      expect(netSpeed.deviceIdx('eth0'), equals(0));\n      expect(netSpeed.deviceIdx('eth1'), equals(1));\n      expect(netSpeed.deviceIdx('nonexistent'), equals(0));\n    });\n  });\n\n  group('NetSpeed real interface prefixes', () {\n    test('Contains all expected prefixes', () {\n      expect(NetSpeed.realIfacePrefixs, contains('eth'));\n      expect(NetSpeed.realIfacePrefixs, contains('wlan'));\n      expect(NetSpeed.realIfacePrefixs, contains('en'));\n      expect(NetSpeed.realIfacePrefixs, contains('ww'));\n      expect(NetSpeed.realIfacePrefixs, contains('wl'));\n    });\n  });\n\n  group('NetSpeed cached values', () {\n    test('Updates cached values on onUpdate', () {\n      final oldData = [\n        NetSpeedPart('eth0', BigInt.from(1000000), BigInt.from(500000), 1000),\n      ];\n      final newData = [\n        NetSpeedPart('eth0', BigInt.from(2000000), BigInt.from(1000000), 2000),\n      ];\n      \n      final netSpeed = NetSpeed(oldData, newData);\n      expect(netSpeed.cachedVals.speedIn, equals('0kb/s'));\n      \n      netSpeed.onUpdate();\n      expect(netSpeed.cachedVals.speedIn, isNot(equals('0kb/s')));\n      expect(netSpeed.cachedVals.speedOut, isNot(equals('0kb/s')));\n      expect(netSpeed.cachedVals.sizeIn, isNot(equals('0kb')));\n      expect(netSpeed.cachedVals.sizeOut, isNot(equals('0kb')));\n    });\n  });\n}"
  },
  {
    "path": "test/nvidia.xml",
    "content": "<?xml version=\"1.0\" ?>\n<!DOCTYPE nvidia_smi_log SYSTEM \"nvsmi_device_v12.dtd\">\n<nvidia_smi_log>\n\t<timestamp>Mon May 20 15:39:05 2024</timestamp>\n\t<driver_version>552.44</driver_version>\n\t<cuda_version>12.4</cuda_version>\n\t<attached_gpus>4</attached_gpus>\n\t<gpu id=\"00000000:01:00.0\">\n\t\t<product_name>NVIDIA GeForce RTX 3090</product_name>\n\t\t<product_brand>GeForce</product_brand>\n\t\t<product_architecture>Ampere</product_architecture>\n\t\t<display_mode>Disabled</display_mode>\n\t\t<display_active>Disabled</display_active>\n\t\t<persistence_mode>Enabled</persistence_mode>\n\t\t<addressing_mode>N/A</addressing_mode>\n\t\t<mig_mode>\n\t\t\t<current_mig>N/A</current_mig>\n\t\t\t<pending_mig>N/A</pending_mig>\n\t\t</mig_mode>\n\t\t<mig_devices>\n\t\t\tNone\n\t\t</mig_devices>\n\t\t<accounting_mode>Disabled</accounting_mode>\n\t\t<accounting_mode_buffer_size>4000</accounting_mode_buffer_size>\n\t\t<driver_model>\n\t\t\t<current_dm>WDDM</current_dm>\n\t\t\t<pending_dm>WDDM</pending_dm>\n\t\t</driver_model>\n\t\t<serial>N/A</serial>\n\t\t<uuid>GPU-1148a571-0e53-c5bd-d400-a99669099c6f</uuid>\n\t\t<minor_number>N/A</minor_number>\n\t\t<vbios_version>94.02.26.88.6d</vbios_version>\n\t\t<multigpu_board>No</multigpu_board>\n\t\t<board_id>0x100</board_id>\n\t\t<board_part_number>N/A</board_part_number>\n\t\t<gpu_part_number>2204-300-A1</gpu_part_number>\n\t\t<gpu_fru_part_number>N/A</gpu_fru_part_number>\n\t\t<gpu_module_id>1</gpu_module_id>\n\t\t<inforom_version>\n\t\t\t<img_version>G001.0000.03.03</img_version>\n\t\t\t<oem_object>2.0</oem_object>\n\t\t\t<ecc_object>N/A</ecc_object>\n\t\t\t<pwr_object>N/A</pwr_object>\n\t\t</inforom_version>\n\t\t<inforom_bbx_flush>\n\t\t\t<latest_timestamp>N/A</latest_timestamp>\n\t\t\t<latest_duration>N/A</latest_duration>\n\t\t</inforom_bbx_flush>\n\t\t<gpu_operation_mode>\n\t\t\t<current_gom>N/A</current_gom>\n\t\t\t<pending_gom>N/A</pending_gom>\n\t\t</gpu_operation_mode>\n\t\t<gsp_firmware_version>N/A</gsp_firmware_version>\n\t\t<gpu_virtualization_mode>\n\t\t\t<virtualization_mode>None</virtualization_mode>\n\t\t\t<host_vgpu_mode>N/A</host_vgpu_mode>\n\t\t</gpu_virtualization_mode>\n\t\t<gpu_reset_status>\n\t\t\t<reset_required>No</reset_required>\n\t\t\t<drain_and_reset_recommended>N/A</drain_and_reset_recommended>\n\t\t</gpu_reset_status>\n\t\t<ibmnpu>\n\t\t\t<relaxed_ordering_mode>N/A</relaxed_ordering_mode>\n\t\t</ibmnpu>\n\t\t<pci>\n\t\t\t<pci_bus>01</pci_bus>\n\t\t\t<pci_device>00</pci_device>\n\t\t\t<pci_domain>0000</pci_domain>\n\t\t\t<pci_device_id>220410DE</pci_device_id>\n\t\t\t<pci_bus_id>00000000:01:00.0</pci_bus_id>\n\t\t\t<pci_sub_system_id>38811462</pci_sub_system_id>\n\t\t\t<pci_gpu_link_info>\n\t\t\t\t<pcie_gen>\n\t\t\t\t\t<max_link_gen>4</max_link_gen>\n\t\t\t\t\t<current_link_gen>4</current_link_gen>\n\t\t\t\t\t<device_current_link_gen>4</device_current_link_gen>\n\t\t\t\t\t<max_device_link_gen>4</max_device_link_gen>\n\t\t\t\t\t<max_host_link_gen>4</max_host_link_gen>\n\t\t\t\t</pcie_gen>\n\t\t\t\t<link_widths>\n\t\t\t\t\t<max_link_width>16x</max_link_width>\n\t\t\t\t\t<current_link_width>16x</current_link_width>\n\t\t\t\t</link_widths>\n\t\t\t</pci_gpu_link_info>\n\t\t\t<pci_bridge_chip>\n\t\t\t\t<bridge_chip_type>N/A</bridge_chip_type>\n\t\t\t\t<bridge_chip_fw>N/A</bridge_chip_fw>\n\t\t\t</pci_bridge_chip>\n\t\t\t<replay_counter>0</replay_counter>\n\t\t\t<replay_rollover_counter>0</replay_rollover_counter>\n\t\t\t<tx_util>0 KB/s</tx_util>\n\t\t\t<rx_util>0 KB/s</rx_util>\n\t\t\t<atomic_caps_inbound>N/A</atomic_caps_inbound>\n\t\t\t<atomic_caps_outbound>N/A</atomic_caps_outbound>\n\t\t</pci>\n\t\t<fan_speed>30 %</fan_speed>\n\t\t<performance_state>P8</performance_state>\n\t\t<clocks_event_reasons>\n\t\t\t<clocks_event_reason_gpu_idle>Active</clocks_event_reason_gpu_idle>\n\t\t\t<clocks_event_reason_applications_clocks_setting>Not Active</clocks_event_reason_applications_clocks_setting>\n\t\t\t<clocks_event_reason_sw_power_cap>Not Active</clocks_event_reason_sw_power_cap>\n\t\t\t<clocks_event_reason_hw_slowdown>Not Active</clocks_event_reason_hw_slowdown>\n\t\t\t<clocks_event_reason_hw_thermal_slowdown>Not Active</clocks_event_reason_hw_thermal_slowdown>\n\t\t\t<clocks_event_reason_hw_power_brake_slowdown>Not Active</clocks_event_reason_hw_power_brake_slowdown>\n\t\t\t<clocks_event_reason_sync_boost>Not Active</clocks_event_reason_sync_boost>\n\t\t\t<clocks_event_reason_sw_thermal_slowdown>Not Active</clocks_event_reason_sw_thermal_slowdown>\n\t\t\t<clocks_event_reason_display_clocks_setting>Not Active</clocks_event_reason_display_clocks_setting>\n\t\t</clocks_event_reasons>\n\t\t<sparse_operation_mode>N/A</sparse_operation_mode>\n\t\t<fb_memory_usage>\n\t\t\t<total>24576 MiB</total>\n\t\t\t<reserved>250 MiB</reserved>\n\t\t\t<used>20601 MiB</used>\n\t\t\t<free>3724 MiB</free>\n\t\t</fb_memory_usage>\n\t\t<bar1_memory_usage>\n\t\t\t<total>256 MiB</total>\n\t\t\t<used>1 MiB</used>\n\t\t\t<free>255 MiB</free>\n\t\t</bar1_memory_usage>\n\t\t<cc_protected_memory_usage>\n\t\t\t<total>N/A</total>\n\t\t\t<used>N/A</used>\n\t\t\t<free>N/A</free>\n\t\t</cc_protected_memory_usage>\n\t\t<compute_mode>Default</compute_mode>\n\t\t<utilization>\n\t\t\t<gpu_util>0 %</gpu_util>\n\t\t\t<memory_util>0 %</memory_util>\n\t\t\t<encoder_util>0 %</encoder_util>\n\t\t\t<decoder_util>0 %</decoder_util>\n\t\t\t<jpeg_util>0 %</jpeg_util>\n\t\t\t<ofa_util>0 %</ofa_util>\n\t\t</utilization>\n\t\t<encoder_stats>\n\t\t\t<session_count>0</session_count>\n\t\t\t<average_fps>0</average_fps>\n\t\t\t<average_latency>0</average_latency>\n\t\t</encoder_stats>\n\t\t<fbc_stats>\n\t\t\t<session_count>0</session_count>\n\t\t\t<average_fps>0</average_fps>\n\t\t\t<average_latency>0</average_latency>\n\t\t</fbc_stats>\n\t\t<ecc_mode>\n\t\t\t<current_ecc>N/A</current_ecc>\n\t\t\t<pending_ecc>N/A</pending_ecc>\n\t\t</ecc_mode>\n\t\t<ecc_errors>\n\t\t\t<volatile>\n\t\t\t\t<sram_correctable>N/A</sram_correctable>\n\t\t\t\t<sram_uncorrectable_parity>N/A</sram_uncorrectable_parity>\n\t\t\t\t<sram_uncorrectable_secded>N/A</sram_uncorrectable_secded>\n\t\t\t\t<dram_correctable>N/A</dram_correctable>\n\t\t\t\t<dram_uncorrectable>N/A</dram_uncorrectable>\n\t\t\t</volatile>\n\t\t\t<aggregate>\n\t\t\t\t<sram_correctable>N/A</sram_correctable>\n\t\t\t\t<sram_uncorrectable_parity>N/A</sram_uncorrectable_parity>\n\t\t\t\t<sram_uncorrectable_secded>N/A</sram_uncorrectable_secded>\n\t\t\t\t<dram_correctable>N/A</dram_correctable>\n\t\t\t\t<dram_uncorrectable>N/A</dram_uncorrectable>\n\t\t\t\t<sram_threshold_exceeded>N/A</sram_threshold_exceeded>\n\t\t\t</aggregate>\n\t\t\t<aggregate_uncorrectable_sram_sources>\n\t\t\t\t<sram_l2>N/A</sram_l2>\n\t\t\t\t<sram_sm>N/A</sram_sm>\n\t\t\t\t<sram_microcontroller>N/A</sram_microcontroller>\n\t\t\t\t<sram_pcie>N/A</sram_pcie>\n\t\t\t\t<sram_other>N/A</sram_other>\n\t\t\t</aggregate_uncorrectable_sram_sources>\n\t\t</ecc_errors>\n\t\t<retired_pages>\n\t\t\t<multiple_single_bit_retirement>\n\t\t\t\t<retired_count>N/A</retired_count>\n\t\t\t\t<retired_pagelist>N/A</retired_pagelist>\n\t\t\t</multiple_single_bit_retirement>\n\t\t\t<double_bit_retirement>\n\t\t\t\t<retired_count>N/A</retired_count>\n\t\t\t\t<retired_pagelist>N/A</retired_pagelist>\n\t\t\t</double_bit_retirement>\n\t\t\t<pending_blacklist>N/A</pending_blacklist>\n\t\t\t<pending_retirement>N/A</pending_retirement>\n\t\t</retired_pages>\n\t\t<remapped_rows>N/A</remapped_rows>\n\t\t<temperature>\n\t\t\t<gpu_temp>37 C</gpu_temp>\n\t\t\t<gpu_temp_tlimit>N/A</gpu_temp_tlimit>\n\t\t\t<gpu_temp_max_threshold>98 C</gpu_temp_max_threshold>\n\t\t\t<gpu_temp_slow_threshold>95 C</gpu_temp_slow_threshold>\n\t\t\t<gpu_temp_max_gpu_threshold>93 C</gpu_temp_max_gpu_threshold>\n\t\t\t<gpu_target_temperature>83 C</gpu_target_temperature>\n\t\t\t<memory_temp>N/A</memory_temp>\n\t\t\t<gpu_temp_max_mem_threshold>N/A</gpu_temp_max_mem_threshold>\n\t\t</temperature>\n\t\t<supported_gpu_target_temp>\n\t\t\t<gpu_target_temp_min>65 C</gpu_target_temp_min>\n\t\t\t<gpu_target_temp_max>91 C</gpu_target_temp_max>\n\t\t</supported_gpu_target_temp>\n\t\t<gpu_power_readings>\n\t\t\t<power_state>P8</power_state>\n\t\t\t<power_draw>13.78 W</power_draw>\n\t\t\t<current_power_limit>350.00 W</current_power_limit>\n\t\t\t<requested_power_limit>350.00 W</requested_power_limit>\n\t\t\t<default_power_limit>350.00 W</default_power_limit>\n\t\t\t<min_power_limit>100.00 W</min_power_limit>\n\t\t\t<max_power_limit>350.00 W</max_power_limit>\n\t\t</gpu_power_readings>\n\t\t<module_power_readings>\n\t\t\t<power_state>P8</power_state>\n\t\t\t<power_draw>N/A</power_draw>\n\t\t\t<current_power_limit>N/A</current_power_limit>\n\t\t\t<requested_power_limit>N/A</requested_power_limit>\n\t\t\t<default_power_limit>N/A</default_power_limit>\n\t\t\t<min_power_limit>N/A</min_power_limit>\n\t\t\t<max_power_limit>N/A</max_power_limit>\n\t\t</module_power_readings>\n\t\t<clocks>\n\t\t\t<graphics_clock>0 MHz</graphics_clock>\n\t\t\t<sm_clock>0 MHz</sm_clock>\n\t\t\t<mem_clock>405 MHz</mem_clock>\n\t\t\t<video_clock>555 MHz</video_clock>\n\t\t</clocks>\n\t\t<applications_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</applications_clocks>\n\t\t<default_applications_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</default_applications_clocks>\n\t\t<deferred_clocks>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</deferred_clocks>\n\t\t<max_clocks>\n\t\t\t<graphics_clock>2100 MHz</graphics_clock>\n\t\t\t<sm_clock>2100 MHz</sm_clock>\n\t\t\t<mem_clock>9751 MHz</mem_clock>\n\t\t\t<video_clock>1950 MHz</video_clock>\n\t\t</max_clocks>\n\t\t<max_customer_boost_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t</max_customer_boost_clocks>\n\t\t<clock_policy>\n\t\t\t<auto_boost>N/A</auto_boost>\n\t\t\t<auto_boost_default>N/A</auto_boost_default>\n\t\t</clock_policy>\n\t\t<voltage>\n\t\t\t<graphics_volt>0.000 mV</graphics_volt>\n\t\t</voltage>\n\t\t<fabric>\n\t\t\t<state>N/A</state>\n\t\t\t<status>N/A</status>\n\t\t</fabric>\n\t\t<supported_clocks>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>9751 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>9501 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>5001 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>810 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>405 MHz</value>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t</supported_clocks>\n\t\t<processes>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>27</pid>\n\t\t\t\t<type>G</type>\n\t\t\t\t<process_name>/Xwayland</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>6344</pid>\n\t\t\t\t<type>C</type>\n\t\t\t\t<process_name>/python3.10</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>9468</pid>\n\t\t\t\t<type>C</type>\n\t\t\t\t<process_name>/python3.10</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t</processes>\n\t\t<accounted_processes>\n\t\t</accounted_processes>\n\t</gpu>\n\n\t<gpu id=\"00000000:41:00.0\">\n\t\t<product_name>NVIDIA GeForce RTX 3090</product_name>\n\t\t<product_brand>GeForce</product_brand>\n\t\t<product_architecture>Ampere</product_architecture>\n\t\t<display_mode>Disabled</display_mode>\n\t\t<display_active>Disabled</display_active>\n\t\t<persistence_mode>Enabled</persistence_mode>\n\t\t<addressing_mode>N/A</addressing_mode>\n\t\t<mig_mode>\n\t\t\t<current_mig>N/A</current_mig>\n\t\t\t<pending_mig>N/A</pending_mig>\n\t\t</mig_mode>\n\t\t<mig_devices>\n\t\t\tNone\n\t\t</mig_devices>\n\t\t<accounting_mode>Disabled</accounting_mode>\n\t\t<accounting_mode_buffer_size>4000</accounting_mode_buffer_size>\n\t\t<driver_model>\n\t\t\t<current_dm>WDDM</current_dm>\n\t\t\t<pending_dm>WDDM</pending_dm>\n\t\t</driver_model>\n\t\t<serial>N/A</serial>\n\t\t<uuid>GPU-c970f93a-4ea3-c11e-81bd-ba79f594ba7a</uuid>\n\t\t<minor_number>N/A</minor_number>\n\t\t<vbios_version>94.02.26.88.6d</vbios_version>\n\t\t<multigpu_board>No</multigpu_board>\n\t\t<board_id>0x4100</board_id>\n\t\t<board_part_number>N/A</board_part_number>\n\t\t<gpu_part_number>2204-300-A1</gpu_part_number>\n\t\t<gpu_fru_part_number>N/A</gpu_fru_part_number>\n\t\t<gpu_module_id>1</gpu_module_id>\n\t\t<inforom_version>\n\t\t\t<img_version>G001.0000.03.03</img_version>\n\t\t\t<oem_object>2.0</oem_object>\n\t\t\t<ecc_object>N/A</ecc_object>\n\t\t\t<pwr_object>N/A</pwr_object>\n\t\t</inforom_version>\n\t\t<inforom_bbx_flush>\n\t\t\t<latest_timestamp>N/A</latest_timestamp>\n\t\t\t<latest_duration>N/A</latest_duration>\n\t\t</inforom_bbx_flush>\n\t\t<gpu_operation_mode>\n\t\t\t<current_gom>N/A</current_gom>\n\t\t\t<pending_gom>N/A</pending_gom>\n\t\t</gpu_operation_mode>\n\t\t<gsp_firmware_version>N/A</gsp_firmware_version>\n\t\t<gpu_virtualization_mode>\n\t\t\t<virtualization_mode>None</virtualization_mode>\n\t\t\t<host_vgpu_mode>N/A</host_vgpu_mode>\n\t\t</gpu_virtualization_mode>\n\t\t<gpu_reset_status>\n\t\t\t<reset_required>No</reset_required>\n\t\t\t<drain_and_reset_recommended>N/A</drain_and_reset_recommended>\n\t\t</gpu_reset_status>\n\t\t<ibmnpu>\n\t\t\t<relaxed_ordering_mode>N/A</relaxed_ordering_mode>\n\t\t</ibmnpu>\n\t\t<pci>\n\t\t\t<pci_bus>41</pci_bus>\n\t\t\t<pci_device>00</pci_device>\n\t\t\t<pci_domain>0000</pci_domain>\n\t\t\t<pci_device_id>220410DE</pci_device_id>\n\t\t\t<pci_bus_id>00000000:41:00.0</pci_bus_id>\n\t\t\t<pci_sub_system_id>38811462</pci_sub_system_id>\n\t\t\t<pci_gpu_link_info>\n\t\t\t\t<pcie_gen>\n\t\t\t\t\t<max_link_gen>4</max_link_gen>\n\t\t\t\t\t<current_link_gen>4</current_link_gen>\n\t\t\t\t\t<device_current_link_gen>4</device_current_link_gen>\n\t\t\t\t\t<max_device_link_gen>4</max_device_link_gen>\n\t\t\t\t\t<max_host_link_gen>4</max_host_link_gen>\n\t\t\t\t</pcie_gen>\n\t\t\t\t<link_widths>\n\t\t\t\t\t<max_link_width>16x</max_link_width>\n\t\t\t\t\t<current_link_width>16x</current_link_width>\n\t\t\t\t</link_widths>\n\t\t\t</pci_gpu_link_info>\n\t\t\t<pci_bridge_chip>\n\t\t\t\t<bridge_chip_type>N/A</bridge_chip_type>\n\t\t\t\t<bridge_chip_fw>N/A</bridge_chip_fw>\n\t\t\t</pci_bridge_chip>\n\t\t\t<replay_counter>0</replay_counter>\n\t\t\t<replay_rollover_counter>0</replay_rollover_counter>\n\t\t\t<tx_util>0 KB/s</tx_util>\n\t\t\t<rx_util>0 KB/s</rx_util>\n\t\t\t<atomic_caps_inbound>N/A</atomic_caps_inbound>\n\t\t\t<atomic_caps_outbound>N/A</atomic_caps_outbound>\n\t\t</pci>\n\t\t<fan_speed>30 %</fan_speed>\n\t\t<performance_state>P8</performance_state>\n\t\t<clocks_event_reasons>\n\t\t\t<clocks_event_reason_gpu_idle>Active</clocks_event_reason_gpu_idle>\n\t\t\t<clocks_event_reason_applications_clocks_setting>Not Active</clocks_event_reason_applications_clocks_setting>\n\t\t\t<clocks_event_reason_sw_power_cap>Not Active</clocks_event_reason_sw_power_cap>\n\t\t\t<clocks_event_reason_hw_slowdown>Not Active</clocks_event_reason_hw_slowdown>\n\t\t\t<clocks_event_reason_hw_thermal_slowdown>Not Active</clocks_event_reason_hw_thermal_slowdown>\n\t\t\t<clocks_event_reason_hw_power_brake_slowdown>Not Active</clocks_event_reason_hw_power_brake_slowdown>\n\t\t\t<clocks_event_reason_sync_boost>Not Active</clocks_event_reason_sync_boost>\n\t\t\t<clocks_event_reason_sw_thermal_slowdown>Not Active</clocks_event_reason_sw_thermal_slowdown>\n\t\t\t<clocks_event_reason_display_clocks_setting>Not Active</clocks_event_reason_display_clocks_setting>\n\t\t</clocks_event_reasons>\n\t\t<sparse_operation_mode>N/A</sparse_operation_mode>\n\t\t<fb_memory_usage>\n\t\t\t<total>24576 MiB</total>\n\t\t\t<reserved>250 MiB</reserved>\n\t\t\t<used>20607 MiB</used>\n\t\t\t<free>3718 MiB</free>\n\t\t</fb_memory_usage>\n\t\t<bar1_memory_usage>\n\t\t\t<total>256 MiB</total>\n\t\t\t<used>1 MiB</used>\n\t\t\t<free>255 MiB</free>\n\t\t</bar1_memory_usage>\n\t\t<cc_protected_memory_usage>\n\t\t\t<total>N/A</total>\n\t\t\t<used>N/A</used>\n\t\t\t<free>N/A</free>\n\t\t</cc_protected_memory_usage>\n\t\t<compute_mode>Default</compute_mode>\n\t\t<utilization>\n\t\t\t<gpu_util>0 %</gpu_util>\n\t\t\t<memory_util>0 %</memory_util>\n\t\t\t<encoder_util>0 %</encoder_util>\n\t\t\t<decoder_util>0 %</decoder_util>\n\t\t\t<jpeg_util>0 %</jpeg_util>\n\t\t\t<ofa_util>0 %</ofa_util>\n\t\t</utilization>\n\t\t<encoder_stats>\n\t\t\t<session_count>0</session_count>\n\t\t\t<average_fps>0</average_fps>\n\t\t\t<average_latency>0</average_latency>\n\t\t</encoder_stats>\n\t\t<fbc_stats>\n\t\t\t<session_count>0</session_count>\n\t\t\t<average_fps>0</average_fps>\n\t\t\t<average_latency>0</average_latency>\n\t\t</fbc_stats>\n\t\t<ecc_mode>\n\t\t\t<current_ecc>N/A</current_ecc>\n\t\t\t<pending_ecc>N/A</pending_ecc>\n\t\t</ecc_mode>\n\t\t<ecc_errors>\n\t\t\t<volatile>\n\t\t\t\t<sram_correctable>N/A</sram_correctable>\n\t\t\t\t<sram_uncorrectable_parity>N/A</sram_uncorrectable_parity>\n\t\t\t\t<sram_uncorrectable_secded>N/A</sram_uncorrectable_secded>\n\t\t\t\t<dram_correctable>N/A</dram_correctable>\n\t\t\t\t<dram_uncorrectable>N/A</dram_uncorrectable>\n\t\t\t</volatile>\n\t\t\t<aggregate>\n\t\t\t\t<sram_correctable>N/A</sram_correctable>\n\t\t\t\t<sram_uncorrectable_parity>N/A</sram_uncorrectable_parity>\n\t\t\t\t<sram_uncorrectable_secded>N/A</sram_uncorrectable_secded>\n\t\t\t\t<dram_correctable>N/A</dram_correctable>\n\t\t\t\t<dram_uncorrectable>N/A</dram_uncorrectable>\n\t\t\t\t<sram_threshold_exceeded>N/A</sram_threshold_exceeded>\n\t\t\t</aggregate>\n\t\t\t<aggregate_uncorrectable_sram_sources>\n\t\t\t\t<sram_l2>N/A</sram_l2>\n\t\t\t\t<sram_sm>N/A</sram_sm>\n\t\t\t\t<sram_microcontroller>N/A</sram_microcontroller>\n\t\t\t\t<sram_pcie>N/A</sram_pcie>\n\t\t\t\t<sram_other>N/A</sram_other>\n\t\t\t</aggregate_uncorrectable_sram_sources>\n\t\t</ecc_errors>\n\t\t<retired_pages>\n\t\t\t<multiple_single_bit_retirement>\n\t\t\t\t<retired_count>N/A</retired_count>\n\t\t\t\t<retired_pagelist>N/A</retired_pagelist>\n\t\t\t</multiple_single_bit_retirement>\n\t\t\t<double_bit_retirement>\n\t\t\t\t<retired_count>N/A</retired_count>\n\t\t\t\t<retired_pagelist>N/A</retired_pagelist>\n\t\t\t</double_bit_retirement>\n\t\t\t<pending_blacklist>N/A</pending_blacklist>\n\t\t\t<pending_retirement>N/A</pending_retirement>\n\t\t</retired_pages>\n\t\t<remapped_rows>N/A</remapped_rows>\n\t\t<temperature>\n\t\t\t<gpu_temp>37 C</gpu_temp>\n\t\t\t<gpu_temp_tlimit>N/A</gpu_temp_tlimit>\n\t\t\t<gpu_temp_max_threshold>98 C</gpu_temp_max_threshold>\n\t\t\t<gpu_temp_slow_threshold>95 C</gpu_temp_slow_threshold>\n\t\t\t<gpu_temp_max_gpu_threshold>93 C</gpu_temp_max_gpu_threshold>\n\t\t\t<gpu_target_temperature>83 C</gpu_target_temperature>\n\t\t\t<memory_temp>N/A</memory_temp>\n\t\t\t<gpu_temp_max_mem_threshold>N/A</gpu_temp_max_mem_threshold>\n\t\t</temperature>\n\t\t<supported_gpu_target_temp>\n\t\t\t<gpu_target_temp_min>65 C</gpu_target_temp_min>\n\t\t\t<gpu_target_temp_max>91 C</gpu_target_temp_max>\n\t\t</supported_gpu_target_temp>\n\t\t<gpu_power_readings>\n\t\t\t<power_state>P8</power_state>\n\t\t\t<power_draw>11.09 W</power_draw>\n\t\t\t<current_power_limit>350.00 W</current_power_limit>\n\t\t\t<requested_power_limit>350.00 W</requested_power_limit>\n\t\t\t<default_power_limit>350.00 W</default_power_limit>\n\t\t\t<min_power_limit>100.00 W</min_power_limit>\n\t\t\t<max_power_limit>350.00 W</max_power_limit>\n\t\t</gpu_power_readings>\n\t\t<module_power_readings>\n\t\t\t<power_state>P8</power_state>\n\t\t\t<power_draw>N/A</power_draw>\n\t\t\t<current_power_limit>N/A</current_power_limit>\n\t\t\t<requested_power_limit>N/A</requested_power_limit>\n\t\t\t<default_power_limit>N/A</default_power_limit>\n\t\t\t<min_power_limit>N/A</min_power_limit>\n\t\t\t<max_power_limit>N/A</max_power_limit>\n\t\t</module_power_readings>\n\t\t<clocks>\n\t\t\t<graphics_clock>0 MHz</graphics_clock>\n\t\t\t<sm_clock>0 MHz</sm_clock>\n\t\t\t<mem_clock>405 MHz</mem_clock>\n\t\t\t<video_clock>555 MHz</video_clock>\n\t\t</clocks>\n\t\t<applications_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</applications_clocks>\n\t\t<default_applications_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</default_applications_clocks>\n\t\t<deferred_clocks>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</deferred_clocks>\n\t\t<max_clocks>\n\t\t\t<graphics_clock>2100 MHz</graphics_clock>\n\t\t\t<sm_clock>2100 MHz</sm_clock>\n\t\t\t<mem_clock>9751 MHz</mem_clock>\n\t\t\t<video_clock>1950 MHz</video_clock>\n\t\t</max_clocks>\n\t\t<max_customer_boost_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t</max_customer_boost_clocks>\n\t\t<clock_policy>\n\t\t\t<auto_boost>N/A</auto_boost>\n\t\t\t<auto_boost_default>N/A</auto_boost_default>\n\t\t</clock_policy>\n\t\t<voltage>\n\t\t\t<graphics_volt>0.000 mV</graphics_volt>\n\t\t</voltage>\n\t\t<fabric>\n\t\t\t<state>N/A</state>\n\t\t\t<status>N/A</status>\n\t\t</fabric>\n\t\t<supported_clocks>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>9751 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>9501 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>5001 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>810 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>405 MHz</value>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t</supported_clocks>\n\t\t<processes>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>27</pid>\n\t\t\t\t<type>G</type>\n\t\t\t\t<process_name>/Xwayland</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>6344</pid>\n\t\t\t\t<type>C</type>\n\t\t\t\t<process_name>/python3.10</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>9468</pid>\n\t\t\t\t<type>C</type>\n\t\t\t\t<process_name>/python3.10</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t</processes>\n\t\t<accounted_processes>\n\t\t</accounted_processes>\n\t</gpu>\n\n\t<gpu id=\"00000000:83:00.0\">\n\t\t<product_name>NVIDIA GeForce RTX 3090</product_name>\n\t\t<product_brand>GeForce</product_brand>\n\t\t<product_architecture>Ampere</product_architecture>\n\t\t<display_mode>Disabled</display_mode>\n\t\t<display_active>Disabled</display_active>\n\t\t<persistence_mode>Enabled</persistence_mode>\n\t\t<addressing_mode>N/A</addressing_mode>\n\t\t<mig_mode>\n\t\t\t<current_mig>N/A</current_mig>\n\t\t\t<pending_mig>N/A</pending_mig>\n\t\t</mig_mode>\n\t\t<mig_devices>\n\t\t\tNone\n\t\t</mig_devices>\n\t\t<accounting_mode>Disabled</accounting_mode>\n\t\t<accounting_mode_buffer_size>4000</accounting_mode_buffer_size>\n\t\t<driver_model>\n\t\t\t<current_dm>WDDM</current_dm>\n\t\t\t<pending_dm>WDDM</pending_dm>\n\t\t</driver_model>\n\t\t<serial>N/A</serial>\n\t\t<uuid>GPU-16aceab8-06c1-4207-b397-fc0b18a5cc2b</uuid>\n\t\t<minor_number>N/A</minor_number>\n\t\t<vbios_version>94.02.26.88.6d</vbios_version>\n\t\t<multigpu_board>No</multigpu_board>\n\t\t<board_id>0x8300</board_id>\n\t\t<board_part_number>N/A</board_part_number>\n\t\t<gpu_part_number>2204-300-A1</gpu_part_number>\n\t\t<gpu_fru_part_number>N/A</gpu_fru_part_number>\n\t\t<gpu_module_id>1</gpu_module_id>\n\t\t<inforom_version>\n\t\t\t<img_version>G001.0000.03.03</img_version>\n\t\t\t<oem_object>2.0</oem_object>\n\t\t\t<ecc_object>N/A</ecc_object>\n\t\t\t<pwr_object>N/A</pwr_object>\n\t\t</inforom_version>\n\t\t<inforom_bbx_flush>\n\t\t\t<latest_timestamp>N/A</latest_timestamp>\n\t\t\t<latest_duration>N/A</latest_duration>\n\t\t</inforom_bbx_flush>\n\t\t<gpu_operation_mode>\n\t\t\t<current_gom>N/A</current_gom>\n\t\t\t<pending_gom>N/A</pending_gom>\n\t\t</gpu_operation_mode>\n\t\t<gsp_firmware_version>N/A</gsp_firmware_version>\n\t\t<gpu_virtualization_mode>\n\t\t\t<virtualization_mode>None</virtualization_mode>\n\t\t\t<host_vgpu_mode>N/A</host_vgpu_mode>\n\t\t</gpu_virtualization_mode>\n\t\t<gpu_reset_status>\n\t\t\t<reset_required>No</reset_required>\n\t\t\t<drain_and_reset_recommended>N/A</drain_and_reset_recommended>\n\t\t</gpu_reset_status>\n\t\t<ibmnpu>\n\t\t\t<relaxed_ordering_mode>N/A</relaxed_ordering_mode>\n\t\t</ibmnpu>\n\t\t<pci>\n\t\t\t<pci_bus>83</pci_bus>\n\t\t\t<pci_device>00</pci_device>\n\t\t\t<pci_domain>0000</pci_domain>\n\t\t\t<pci_device_id>220410DE</pci_device_id>\n\t\t\t<pci_bus_id>00000000:83:00.0</pci_bus_id>\n\t\t\t<pci_sub_system_id>38811462</pci_sub_system_id>\n\t\t\t<pci_gpu_link_info>\n\t\t\t\t<pcie_gen>\n\t\t\t\t\t<max_link_gen>4</max_link_gen>\n\t\t\t\t\t<current_link_gen>4</current_link_gen>\n\t\t\t\t\t<device_current_link_gen>4</device_current_link_gen>\n\t\t\t\t\t<max_device_link_gen>4</max_device_link_gen>\n\t\t\t\t\t<max_host_link_gen>4</max_host_link_gen>\n\t\t\t\t</pcie_gen>\n\t\t\t\t<link_widths>\n\t\t\t\t\t<max_link_width>16x</max_link_width>\n\t\t\t\t\t<current_link_width>8x</current_link_width>\n\t\t\t\t</link_widths>\n\t\t\t</pci_gpu_link_info>\n\t\t\t<pci_bridge_chip>\n\t\t\t\t<bridge_chip_type>N/A</bridge_chip_type>\n\t\t\t\t<bridge_chip_fw>N/A</bridge_chip_fw>\n\t\t\t</pci_bridge_chip>\n\t\t\t<replay_counter>0</replay_counter>\n\t\t\t<replay_rollover_counter>0</replay_rollover_counter>\n\t\t\t<tx_util>0 KB/s</tx_util>\n\t\t\t<rx_util>0 KB/s</rx_util>\n\t\t\t<atomic_caps_inbound>N/A</atomic_caps_inbound>\n\t\t\t<atomic_caps_outbound>N/A</atomic_caps_outbound>\n\t\t</pci>\n\t\t<fan_speed>30 %</fan_speed>\n\t\t<performance_state>P8</performance_state>\n\t\t<clocks_event_reasons>\n\t\t\t<clocks_event_reason_gpu_idle>Active</clocks_event_reason_gpu_idle>\n\t\t\t<clocks_event_reason_applications_clocks_setting>Not Active</clocks_event_reason_applications_clocks_setting>\n\t\t\t<clocks_event_reason_sw_power_cap>Not Active</clocks_event_reason_sw_power_cap>\n\t\t\t<clocks_event_reason_hw_slowdown>Not Active</clocks_event_reason_hw_slowdown>\n\t\t\t<clocks_event_reason_hw_thermal_slowdown>Not Active</clocks_event_reason_hw_thermal_slowdown>\n\t\t\t<clocks_event_reason_hw_power_brake_slowdown>Not Active</clocks_event_reason_hw_power_brake_slowdown>\n\t\t\t<clocks_event_reason_sync_boost>Not Active</clocks_event_reason_sync_boost>\n\t\t\t<clocks_event_reason_sw_thermal_slowdown>Not Active</clocks_event_reason_sw_thermal_slowdown>\n\t\t\t<clocks_event_reason_display_clocks_setting>Not Active</clocks_event_reason_display_clocks_setting>\n\t\t</clocks_event_reasons>\n\t\t<sparse_operation_mode>N/A</sparse_operation_mode>\n\t\t<fb_memory_usage>\n\t\t\t<total>24576 MiB</total>\n\t\t\t<reserved>250 MiB</reserved>\n\t\t\t<used>2547 MiB</used>\n\t\t\t<free>21778 MiB</free>\n\t\t</fb_memory_usage>\n\t\t<bar1_memory_usage>\n\t\t\t<total>256 MiB</total>\n\t\t\t<used>1 MiB</used>\n\t\t\t<free>255 MiB</free>\n\t\t</bar1_memory_usage>\n\t\t<cc_protected_memory_usage>\n\t\t\t<total>N/A</total>\n\t\t\t<used>N/A</used>\n\t\t\t<free>N/A</free>\n\t\t</cc_protected_memory_usage>\n\t\t<compute_mode>Default</compute_mode>\n\t\t<utilization>\n\t\t\t<gpu_util>0 %</gpu_util>\n\t\t\t<memory_util>0 %</memory_util>\n\t\t\t<encoder_util>0 %</encoder_util>\n\t\t\t<decoder_util>0 %</decoder_util>\n\t\t\t<jpeg_util>0 %</jpeg_util>\n\t\t\t<ofa_util>0 %</ofa_util>\n\t\t</utilization>\n\t\t<encoder_stats>\n\t\t\t<session_count>0</session_count>\n\t\t\t<average_fps>0</average_fps>\n\t\t\t<average_latency>0</average_latency>\n\t\t</encoder_stats>\n\t\t<fbc_stats>\n\t\t\t<session_count>0</session_count>\n\t\t\t<average_fps>0</average_fps>\n\t\t\t<average_latency>0</average_latency>\n\t\t</fbc_stats>\n\t\t<ecc_mode>\n\t\t\t<current_ecc>N/A</current_ecc>\n\t\t\t<pending_ecc>N/A</pending_ecc>\n\t\t</ecc_mode>\n\t\t<ecc_errors>\n\t\t\t<volatile>\n\t\t\t\t<sram_correctable>N/A</sram_correctable>\n\t\t\t\t<sram_uncorrectable_parity>N/A</sram_uncorrectable_parity>\n\t\t\t\t<sram_uncorrectable_secded>N/A</sram_uncorrectable_secded>\n\t\t\t\t<dram_correctable>N/A</dram_correctable>\n\t\t\t\t<dram_uncorrectable>N/A</dram_uncorrectable>\n\t\t\t</volatile>\n\t\t\t<aggregate>\n\t\t\t\t<sram_correctable>N/A</sram_correctable>\n\t\t\t\t<sram_uncorrectable_parity>N/A</sram_uncorrectable_parity>\n\t\t\t\t<sram_uncorrectable_secded>N/A</sram_uncorrectable_secded>\n\t\t\t\t<dram_correctable>N/A</dram_correctable>\n\t\t\t\t<dram_uncorrectable>N/A</dram_uncorrectable>\n\t\t\t\t<sram_threshold_exceeded>N/A</sram_threshold_exceeded>\n\t\t\t</aggregate>\n\t\t\t<aggregate_uncorrectable_sram_sources>\n\t\t\t\t<sram_l2>N/A</sram_l2>\n\t\t\t\t<sram_sm>N/A</sram_sm>\n\t\t\t\t<sram_microcontroller>N/A</sram_microcontroller>\n\t\t\t\t<sram_pcie>N/A</sram_pcie>\n\t\t\t\t<sram_other>N/A</sram_other>\n\t\t\t</aggregate_uncorrectable_sram_sources>\n\t\t</ecc_errors>\n\t\t<retired_pages>\n\t\t\t<multiple_single_bit_retirement>\n\t\t\t\t<retired_count>N/A</retired_count>\n\t\t\t\t<retired_pagelist>N/A</retired_pagelist>\n\t\t\t</multiple_single_bit_retirement>\n\t\t\t<double_bit_retirement>\n\t\t\t\t<retired_count>N/A</retired_count>\n\t\t\t\t<retired_pagelist>N/A</retired_pagelist>\n\t\t\t</double_bit_retirement>\n\t\t\t<pending_blacklist>N/A</pending_blacklist>\n\t\t\t<pending_retirement>N/A</pending_retirement>\n\t\t</retired_pages>\n\t\t<remapped_rows>N/A</remapped_rows>\n\t\t<temperature>\n\t\t\t<gpu_temp>35 C</gpu_temp>\n\t\t\t<gpu_temp_tlimit>N/A</gpu_temp_tlimit>\n\t\t\t<gpu_temp_max_threshold>98 C</gpu_temp_max_threshold>\n\t\t\t<gpu_temp_slow_threshold>95 C</gpu_temp_slow_threshold>\n\t\t\t<gpu_temp_max_gpu_threshold>93 C</gpu_temp_max_gpu_threshold>\n\t\t\t<gpu_target_temperature>83 C</gpu_target_temperature>\n\t\t\t<memory_temp>N/A</memory_temp>\n\t\t\t<gpu_temp_max_mem_threshold>N/A</gpu_temp_max_mem_threshold>\n\t\t</temperature>\n\t\t<supported_gpu_target_temp>\n\t\t\t<gpu_target_temp_min>65 C</gpu_target_temp_min>\n\t\t\t<gpu_target_temp_max>91 C</gpu_target_temp_max>\n\t\t</supported_gpu_target_temp>\n\t\t<gpu_power_readings>\n\t\t\t<power_state>P8</power_state>\n\t\t\t<power_draw>6.60 W</power_draw>\n\t\t\t<current_power_limit>350.00 W</current_power_limit>\n\t\t\t<requested_power_limit>350.00 W</requested_power_limit>\n\t\t\t<default_power_limit>350.00 W</default_power_limit>\n\t\t\t<min_power_limit>100.00 W</min_power_limit>\n\t\t\t<max_power_limit>350.00 W</max_power_limit>\n\t\t</gpu_power_readings>\n\t\t<module_power_readings>\n\t\t\t<power_state>P8</power_state>\n\t\t\t<power_draw>N/A</power_draw>\n\t\t\t<current_power_limit>N/A</current_power_limit>\n\t\t\t<requested_power_limit>N/A</requested_power_limit>\n\t\t\t<default_power_limit>N/A</default_power_limit>\n\t\t\t<min_power_limit>N/A</min_power_limit>\n\t\t\t<max_power_limit>N/A</max_power_limit>\n\t\t</module_power_readings>\n\t\t<clocks>\n\t\t\t<graphics_clock>0 MHz</graphics_clock>\n\t\t\t<sm_clock>0 MHz</sm_clock>\n\t\t\t<mem_clock>405 MHz</mem_clock>\n\t\t\t<video_clock>555 MHz</video_clock>\n\t\t</clocks>\n\t\t<applications_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</applications_clocks>\n\t\t<default_applications_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</default_applications_clocks>\n\t\t<deferred_clocks>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</deferred_clocks>\n\t\t<max_clocks>\n\t\t\t<graphics_clock>2100 MHz</graphics_clock>\n\t\t\t<sm_clock>2100 MHz</sm_clock>\n\t\t\t<mem_clock>9751 MHz</mem_clock>\n\t\t\t<video_clock>1950 MHz</video_clock>\n\t\t</max_clocks>\n\t\t<max_customer_boost_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t</max_customer_boost_clocks>\n\t\t<clock_policy>\n\t\t\t<auto_boost>N/A</auto_boost>\n\t\t\t<auto_boost_default>N/A</auto_boost_default>\n\t\t</clock_policy>\n\t\t<voltage>\n\t\t\t<graphics_volt>0.000 mV</graphics_volt>\n\t\t</voltage>\n\t\t<fabric>\n\t\t\t<state>N/A</state>\n\t\t\t<status>N/A</status>\n\t\t</fabric>\n\t\t<supported_clocks>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>9751 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>9501 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>5001 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>810 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>405 MHz</value>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t</supported_clocks>\n\t\t<processes>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>27</pid>\n\t\t\t\t<type>G</type>\n\t\t\t\t<process_name>/Xwayland</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>6344</pid>\n\t\t\t\t<type>C</type>\n\t\t\t\t<process_name>/python3.10</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>9468</pid>\n\t\t\t\t<type>C</type>\n\t\t\t\t<process_name>/python3.10</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t</processes>\n\t\t<accounted_processes>\n\t\t</accounted_processes>\n\t</gpu>\n\n\t<gpu id=\"00000000:84:00.0\">\n\t\t<product_name>NVIDIA GeForce RTX 3090</product_name>\n\t\t<product_brand>GeForce</product_brand>\n\t\t<product_architecture>Ampere</product_architecture>\n\t\t<display_mode>Enabled</display_mode>\n\t\t<display_active>Enabled</display_active>\n\t\t<persistence_mode>Enabled</persistence_mode>\n\t\t<addressing_mode>N/A</addressing_mode>\n\t\t<mig_mode>\n\t\t\t<current_mig>N/A</current_mig>\n\t\t\t<pending_mig>N/A</pending_mig>\n\t\t</mig_mode>\n\t\t<mig_devices>\n\t\t\tNone\n\t\t</mig_devices>\n\t\t<accounting_mode>Disabled</accounting_mode>\n\t\t<accounting_mode_buffer_size>4000</accounting_mode_buffer_size>\n\t\t<driver_model>\n\t\t\t<current_dm>WDDM</current_dm>\n\t\t\t<pending_dm>WDDM</pending_dm>\n\t\t</driver_model>\n\t\t<serial>N/A</serial>\n\t\t<uuid>GPU-cecdeb62-a1c8-cac6-8399-47b2e4cd44d3</uuid>\n\t\t<minor_number>N/A</minor_number>\n\t\t<vbios_version>94.02.26.88.6d</vbios_version>\n\t\t<multigpu_board>No</multigpu_board>\n\t\t<board_id>0x8400</board_id>\n\t\t<board_part_number>N/A</board_part_number>\n\t\t<gpu_part_number>2204-300-A1</gpu_part_number>\n\t\t<gpu_fru_part_number>N/A</gpu_fru_part_number>\n\t\t<gpu_module_id>1</gpu_module_id>\n\t\t<inforom_version>\n\t\t\t<img_version>G001.0000.03.03</img_version>\n\t\t\t<oem_object>2.0</oem_object>\n\t\t\t<ecc_object>N/A</ecc_object>\n\t\t\t<pwr_object>N/A</pwr_object>\n\t\t</inforom_version>\n\t\t<inforom_bbx_flush>\n\t\t\t<latest_timestamp>N/A</latest_timestamp>\n\t\t\t<latest_duration>N/A</latest_duration>\n\t\t</inforom_bbx_flush>\n\t\t<gpu_operation_mode>\n\t\t\t<current_gom>N/A</current_gom>\n\t\t\t<pending_gom>N/A</pending_gom>\n\t\t</gpu_operation_mode>\n\t\t<gsp_firmware_version>N/A</gsp_firmware_version>\n\t\t<gpu_virtualization_mode>\n\t\t\t<virtualization_mode>None</virtualization_mode>\n\t\t\t<host_vgpu_mode>N/A</host_vgpu_mode>\n\t\t</gpu_virtualization_mode>\n\t\t<gpu_reset_status>\n\t\t\t<reset_required>No</reset_required>\n\t\t\t<drain_and_reset_recommended>N/A</drain_and_reset_recommended>\n\t\t</gpu_reset_status>\n\t\t<ibmnpu>\n\t\t\t<relaxed_ordering_mode>N/A</relaxed_ordering_mode>\n\t\t</ibmnpu>\n\t\t<pci>\n\t\t\t<pci_bus>84</pci_bus>\n\t\t\t<pci_device>00</pci_device>\n\t\t\t<pci_domain>0000</pci_domain>\n\t\t\t<pci_device_id>220410DE</pci_device_id>\n\t\t\t<pci_bus_id>00000000:84:00.0</pci_bus_id>\n\t\t\t<pci_sub_system_id>38811462</pci_sub_system_id>\n\t\t\t<pci_gpu_link_info>\n\t\t\t\t<pcie_gen>\n\t\t\t\t\t<max_link_gen>4</max_link_gen>\n\t\t\t\t\t<current_link_gen>4</current_link_gen>\n\t\t\t\t\t<device_current_link_gen>4</device_current_link_gen>\n\t\t\t\t\t<max_device_link_gen>4</max_device_link_gen>\n\t\t\t\t\t<max_host_link_gen>4</max_host_link_gen>\n\t\t\t\t</pcie_gen>\n\t\t\t\t<link_widths>\n\t\t\t\t\t<max_link_width>16x</max_link_width>\n\t\t\t\t\t<current_link_width>16x</current_link_width>\n\t\t\t\t</link_widths>\n\t\t\t</pci_gpu_link_info>\n\t\t\t<pci_bridge_chip>\n\t\t\t\t<bridge_chip_type>N/A</bridge_chip_type>\n\t\t\t\t<bridge_chip_fw>N/A</bridge_chip_fw>\n\t\t\t</pci_bridge_chip>\n\t\t\t<replay_counter>0</replay_counter>\n\t\t\t<replay_rollover_counter>0</replay_rollover_counter>\n\t\t\t<tx_util>0 KB/s</tx_util>\n\t\t\t<rx_util>0 KB/s</rx_util>\n\t\t\t<atomic_caps_inbound>N/A</atomic_caps_inbound>\n\t\t\t<atomic_caps_outbound>N/A</atomic_caps_outbound>\n\t\t</pci>\n\t\t<fan_speed>30 %</fan_speed>\n\t\t<performance_state>P8</performance_state>\n\t\t<clocks_event_reasons>\n\t\t\t<clocks_event_reason_gpu_idle>Active</clocks_event_reason_gpu_idle>\n\t\t\t<clocks_event_reason_applications_clocks_setting>Not Active</clocks_event_reason_applications_clocks_setting>\n\t\t\t<clocks_event_reason_sw_power_cap>Not Active</clocks_event_reason_sw_power_cap>\n\t\t\t<clocks_event_reason_hw_slowdown>Not Active</clocks_event_reason_hw_slowdown>\n\t\t\t<clocks_event_reason_hw_thermal_slowdown>Not Active</clocks_event_reason_hw_thermal_slowdown>\n\t\t\t<clocks_event_reason_hw_power_brake_slowdown>Not Active</clocks_event_reason_hw_power_brake_slowdown>\n\t\t\t<clocks_event_reason_sync_boost>Not Active</clocks_event_reason_sync_boost>\n\t\t\t<clocks_event_reason_sw_thermal_slowdown>Not Active</clocks_event_reason_sw_thermal_slowdown>\n\t\t\t<clocks_event_reason_display_clocks_setting>Not Active</clocks_event_reason_display_clocks_setting>\n\t\t</clocks_event_reasons>\n\t\t<sparse_operation_mode>N/A</sparse_operation_mode>\n\t\t<fb_memory_usage>\n\t\t\t<total>24576 MiB</total>\n\t\t\t<reserved>250 MiB</reserved>\n\t\t\t<used>1187 MiB</used>\n\t\t\t<free>23138 MiB</free>\n\t\t</fb_memory_usage>\n\t\t<bar1_memory_usage>\n\t\t\t<total>256 MiB</total>\n\t\t\t<used>1 MiB</used>\n\t\t\t<free>255 MiB</free>\n\t\t</bar1_memory_usage>\n\t\t<cc_protected_memory_usage>\n\t\t\t<total>N/A</total>\n\t\t\t<used>N/A</used>\n\t\t\t<free>N/A</free>\n\t\t</cc_protected_memory_usage>\n\t\t<compute_mode>Default</compute_mode>\n\t\t<utilization>\n\t\t\t<gpu_util>3 %</gpu_util>\n\t\t\t<memory_util>5 %</memory_util>\n\t\t\t<encoder_util>0 %</encoder_util>\n\t\t\t<decoder_util>0 %</decoder_util>\n\t\t\t<jpeg_util>0 %</jpeg_util>\n\t\t\t<ofa_util>0 %</ofa_util>\n\t\t</utilization>\n\t\t<encoder_stats>\n\t\t\t<session_count>0</session_count>\n\t\t\t<average_fps>0</average_fps>\n\t\t\t<average_latency>0</average_latency>\n\t\t</encoder_stats>\n\t\t<fbc_stats>\n\t\t\t<session_count>0</session_count>\n\t\t\t<average_fps>0</average_fps>\n\t\t\t<average_latency>0</average_latency>\n\t\t</fbc_stats>\n\t\t<ecc_mode>\n\t\t\t<current_ecc>N/A</current_ecc>\n\t\t\t<pending_ecc>N/A</pending_ecc>\n\t\t</ecc_mode>\n\t\t<ecc_errors>\n\t\t\t<volatile>\n\t\t\t\t<sram_correctable>N/A</sram_correctable>\n\t\t\t\t<sram_uncorrectable_parity>N/A</sram_uncorrectable_parity>\n\t\t\t\t<sram_uncorrectable_secded>N/A</sram_uncorrectable_secded>\n\t\t\t\t<dram_correctable>N/A</dram_correctable>\n\t\t\t\t<dram_uncorrectable>N/A</dram_uncorrectable>\n\t\t\t</volatile>\n\t\t\t<aggregate>\n\t\t\t\t<sram_correctable>N/A</sram_correctable>\n\t\t\t\t<sram_uncorrectable_parity>N/A</sram_uncorrectable_parity>\n\t\t\t\t<sram_uncorrectable_secded>N/A</sram_uncorrectable_secded>\n\t\t\t\t<dram_correctable>N/A</dram_correctable>\n\t\t\t\t<dram_uncorrectable>N/A</dram_uncorrectable>\n\t\t\t\t<sram_threshold_exceeded>N/A</sram_threshold_exceeded>\n\t\t\t</aggregate>\n\t\t\t<aggregate_uncorrectable_sram_sources>\n\t\t\t\t<sram_l2>N/A</sram_l2>\n\t\t\t\t<sram_sm>N/A</sram_sm>\n\t\t\t\t<sram_microcontroller>N/A</sram_microcontroller>\n\t\t\t\t<sram_pcie>N/A</sram_pcie>\n\t\t\t\t<sram_other>N/A</sram_other>\n\t\t\t</aggregate_uncorrectable_sram_sources>\n\t\t</ecc_errors>\n\t\t<retired_pages>\n\t\t\t<multiple_single_bit_retirement>\n\t\t\t\t<retired_count>N/A</retired_count>\n\t\t\t\t<retired_pagelist>N/A</retired_pagelist>\n\t\t\t</multiple_single_bit_retirement>\n\t\t\t<double_bit_retirement>\n\t\t\t\t<retired_count>N/A</retired_count>\n\t\t\t\t<retired_pagelist>N/A</retired_pagelist>\n\t\t\t</double_bit_retirement>\n\t\t\t<pending_blacklist>N/A</pending_blacklist>\n\t\t\t<pending_retirement>N/A</pending_retirement>\n\t\t</retired_pages>\n\t\t<remapped_rows>N/A</remapped_rows>\n\t\t<temperature>\n\t\t\t<gpu_temp>32 C</gpu_temp>\n\t\t\t<gpu_temp_tlimit>N/A</gpu_temp_tlimit>\n\t\t\t<gpu_temp_max_threshold>98 C</gpu_temp_max_threshold>\n\t\t\t<gpu_temp_slow_threshold>95 C</gpu_temp_slow_threshold>\n\t\t\t<gpu_temp_max_gpu_threshold>93 C</gpu_temp_max_gpu_threshold>\n\t\t\t<gpu_target_temperature>83 C</gpu_target_temperature>\n\t\t\t<memory_temp>N/A</memory_temp>\n\t\t\t<gpu_temp_max_mem_threshold>N/A</gpu_temp_max_mem_threshold>\n\t\t</temperature>\n\t\t<supported_gpu_target_temp>\n\t\t\t<gpu_target_temp_min>65 C</gpu_target_temp_min>\n\t\t\t<gpu_target_temp_max>91 C</gpu_target_temp_max>\n\t\t</supported_gpu_target_temp>\n\t\t<gpu_power_readings>\n\t\t\t<power_state>P8</power_state>\n\t\t\t<power_draw>15.30 W</power_draw>\n\t\t\t<current_power_limit>350.00 W</current_power_limit>\n\t\t\t<requested_power_limit>350.00 W</requested_power_limit>\n\t\t\t<default_power_limit>350.00 W</default_power_limit>\n\t\t\t<min_power_limit>100.00 W</min_power_limit>\n\t\t\t<max_power_limit>350.00 W</max_power_limit>\n\t\t</gpu_power_readings>\n\t\t<module_power_readings>\n\t\t\t<power_state>P8</power_state>\n\t\t\t<power_draw>N/A</power_draw>\n\t\t\t<current_power_limit>N/A</current_power_limit>\n\t\t\t<requested_power_limit>N/A</requested_power_limit>\n\t\t\t<default_power_limit>N/A</default_power_limit>\n\t\t\t<min_power_limit>N/A</min_power_limit>\n\t\t\t<max_power_limit>N/A</max_power_limit>\n\t\t</module_power_readings>\n\t\t<clocks>\n\t\t\t<graphics_clock>210 MHz</graphics_clock>\n\t\t\t<sm_clock>210 MHz</sm_clock>\n\t\t\t<mem_clock>405 MHz</mem_clock>\n\t\t\t<video_clock>555 MHz</video_clock>\n\t\t</clocks>\n\t\t<applications_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</applications_clocks>\n\t\t<default_applications_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</default_applications_clocks>\n\t\t<deferred_clocks>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t</deferred_clocks>\n\t\t<max_clocks>\n\t\t\t<graphics_clock>2100 MHz</graphics_clock>\n\t\t\t<sm_clock>2100 MHz</sm_clock>\n\t\t\t<mem_clock>9751 MHz</mem_clock>\n\t\t\t<video_clock>1950 MHz</video_clock>\n\t\t</max_clocks>\n\t\t<max_customer_boost_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t</max_customer_boost_clocks>\n\t\t<clock_policy>\n\t\t\t<auto_boost>N/A</auto_boost>\n\t\t\t<auto_boost_default>N/A</auto_boost_default>\n\t\t</clock_policy>\n\t\t<voltage>\n\t\t\t<graphics_volt>743.750 mV</graphics_volt>\n\t\t</voltage>\n\t\t<fabric>\n\t\t\t<state>N/A</state>\n\t\t\t<status>N/A</status>\n\t\t</fabric>\n\t\t<supported_clocks>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>9751 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>9501 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>5001 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>810 MHz</value>\n\t\t\t\t<supported_graphics_clock>2100 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2085 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2070 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2055 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2040 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2025 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>2010 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1995 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1980 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1965 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1950 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1935 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1920 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1905 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1890 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1860 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1845 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1830 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1815 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1800 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1785 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1770 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1755 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1740 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1725 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1710 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1695 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1680 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1665 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1650 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1635 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1620 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1605 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1590 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1575 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1560 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1545 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1530 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1515 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1500 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1485 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1470 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1455 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1440 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1425 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1410 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1395 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1365 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1350 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1320 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1305 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1260 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1215 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1170 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1125 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1110 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1095 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1080 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1065 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1050 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1035 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1020 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>1005 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>990 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>975 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>960 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>945 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>930 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>915 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>900 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>885 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>870 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>855 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>840 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>825 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>795 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>780 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>765 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>750 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>735 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>720 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>690 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>675 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>660 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>645 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>630 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>615 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>600 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>585 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>570 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>555 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>540 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>525 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>510 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>495 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>480 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>465 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>450 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>435 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>405 MHz</value>\n\t\t\t\t<supported_graphics_clock>420 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>375 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>360 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>330 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>315 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>285 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>270 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>240 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>225 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t</supported_clocks>\n\t\t<processes>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>27</pid>\n\t\t\t\t<type>G</type>\n\t\t\t\t<process_name>/Xwayland</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>6344</pid>\n\t\t\t\t<type>C</type>\n\t\t\t\t<process_name>/python3.10</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t\t<process_info>\n\t\t\t\t<gpu_instance_id>N/A</gpu_instance_id>\n\t\t\t\t<compute_instance_id>N/A</compute_instance_id>\n\t\t\t\t<pid>9468</pid>\n\t\t\t\t<type>C</type>\n\t\t\t\t<process_name>/python3.10</process_name>\n\t\t\t\t<used_memory>N/A</used_memory>\n\t\t\t</process_info>\n\t\t</processes>\n\t\t<accounted_processes>\n\t\t</accounted_processes>\n\t</gpu>\n\n</nvidia_smi_log>\n"
  },
  {
    "path": "test/nvidia2.xml",
    "content": "<?xml version=\"1.0\" ?>\n<!DOCTYPE nvidia_smi_log SYSTEM \"nvsmi_device_v11.dtd\">\n<nvidia_smi_log>\n\t<timestamp>Thu May 23 19:40:25 2024</timestamp>\n\t<driver_version>470.239.06</driver_version>\n\t<cuda_version>11.4</cuda_version>\n\t<attached_gpus>1</attached_gpus>\n\t<gpu id=\"00000000:0D:00.0\">\n\t\t<product_name>NVIDIA GeForce GT 730</product_name>\n\t\t<product_brand>GeForce</product_brand>\n\t\t<display_mode>N/A</display_mode>\n\t\t<display_active>N/A</display_active>\n\t\t<persistence_mode>Disabled</persistence_mode>\n\t\t<mig_mode>\n\t\t\t<current_mig>N/A</current_mig>\n\t\t\t<pending_mig>N/A</pending_mig>\n\t\t</mig_mode>\n\t\t<mig_devices>\n\t\t\tNone\n\t\t</mig_devices>\n\t\t<accounting_mode>N/A</accounting_mode>\n\t\t<accounting_mode_buffer_size>N/A</accounting_mode_buffer_size>\n\t\t<driver_model>\n\t\t\t<current_dm>N/A</current_dm>\n\t\t\t<pending_dm>N/A</pending_dm>\n\t\t</driver_model>\n\t\t<serial>N/A</serial>\n\t\t<uuid>GPU-3c0451a4-3ef8-e225-7fac-5159712b7d16</uuid>\n\t\t<minor_number>0</minor_number>\n\t\t<vbios_version>80.28.78.00.EA</vbios_version>\n\t\t<multigpu_board>N/A</multigpu_board>\n\t\t<board_id>N/A</board_id>\n\t\t<gpu_part_number>N/A</gpu_part_number>\n\t\t<gpu_module_id>0</gpu_module_id>\n\t\t<inforom_version>\n\t\t\t<img_version>N/A</img_version>\n\t\t\t<oem_object>N/A</oem_object>\n\t\t\t<ecc_object>N/A</ecc_object>\n\t\t\t<pwr_object>N/A</pwr_object>\n\t\t</inforom_version>\n\t\t<gpu_operation_mode>\n\t\t\t<current_gom>N/A</current_gom>\n\t\t\t<pending_gom>N/A</pending_gom>\n\t\t</gpu_operation_mode>\n\t\t<gsp_firmware_version>N/A</gsp_firmware_version>\n\t\t<gpu_virtualization_mode>\n\t\t\t<virtualization_mode>N/A</virtualization_mode>\n\t\t\t<host_vgpu_mode>N/A</host_vgpu_mode>\n\t\t</gpu_virtualization_mode>\n\t\t<ibmnpu>\n\t\t\t<relaxed_ordering_mode>N/A</relaxed_ordering_mode>\n\t\t</ibmnpu>\n\t\t<pci>\n\t\t\t<pci_bus>0D</pci_bus>\n\t\t\t<pci_device>00</pci_device>\n\t\t\t<pci_domain>0000</pci_domain>\n\t\t\t<pci_device_id>128710DE</pci_device_id>\n\t\t\t<pci_bus_id>00000000:0D:00.0</pci_bus_id>\n\t\t\t<pci_sub_system_id>000010DE</pci_sub_system_id>\n\t\t\t<pci_gpu_link_info>\n\t\t\t\t<pcie_gen>\n\t\t\t\t\t<max_link_gen>N/A</max_link_gen>\n\t\t\t\t\t<current_link_gen>N/A</current_link_gen>\n\t\t\t\t</pcie_gen>\n\t\t\t\t<link_widths>\n\t\t\t\t\t<max_link_width>N/A</max_link_width>\n\t\t\t\t\t<current_link_width>N/A</current_link_width>\n\t\t\t\t</link_widths>\n\t\t\t</pci_gpu_link_info>\n\t\t\t<pci_bridge_chip>\n\t\t\t\t<bridge_chip_type>N/A</bridge_chip_type>\n\t\t\t\t<bridge_chip_fw>N/A</bridge_chip_fw>\n\t\t\t</pci_bridge_chip>\n\t\t\t<replay_counter>0</replay_counter>\n\t\t\t<replay_rollover_counter>0</replay_rollover_counter>\n\t\t\t<tx_util>N/A</tx_util>\n\t\t\t<rx_util>N/A</rx_util>\n\t\t</pci>\n\t\t<fan_speed>40 %</fan_speed>\n\t\t<performance_state>P0</performance_state>\n\t\t<clocks_throttle_reasons>N/A</clocks_throttle_reasons>\n\t\t<fb_memory_usage>\n\t\t\t<total>978 MiB</total>\n\t\t\t<used>0 MiB</used>\n\t\t\t<free>978 MiB</free>\n\t\t</fb_memory_usage>\n\t\t<bar1_memory_usage>\n\t\t\t<total>N/A</total>\n\t\t\t<used>N/A</used>\n\t\t\t<free>N/A</free>\n\t\t</bar1_memory_usage>\n\t\t<compute_mode>Default</compute_mode>\n\t\t<utilization>\n\t\t\t<gpu_util>N/A</gpu_util>\n\t\t\t<memory_util>N/A</memory_util>\n\t\t\t<encoder_util>N/A</encoder_util>\n\t\t\t<decoder_util>N/A</decoder_util>\n\t\t</utilization>\n\t\t<encoder_stats>\n\t\t\t<session_count>N/A</session_count>\n\t\t\t<average_fps>N/A</average_fps>\n\t\t\t<average_latency>N/A</average_latency>\n\t\t</encoder_stats>\n\t\t<fbc_stats>\n\t\t\t<session_count>N/A</session_count>\n\t\t\t<average_fps>N/A</average_fps>\n\t\t\t<average_latency>N/A</average_latency>\n\t\t</fbc_stats>\n\t\t<ecc_mode>\n\t\t\t<current_ecc>N/A</current_ecc>\n\t\t\t<pending_ecc>N/A</pending_ecc>\n\t\t</ecc_mode>\n\t\t<ecc_errors>\n\t\t\t<volatile>\n\t\t\t\t<single_bit>\n\t\t\t\t\t<device_memory>N/A</device_memory>\n\t\t\t\t\t<register_file>N/A</register_file>\n\t\t\t\t\t<l1_cache>N/A</l1_cache>\n\t\t\t\t\t<l2_cache>N/A</l2_cache>\n\t\t\t\t\t<texture_memory>N/A</texture_memory>\n\t\t\t\t\t<texture_shm>N/A</texture_shm>\n\t\t\t\t\t<cbu>N/A</cbu>\n\t\t\t\t\t<total>N/A</total>\n\t\t\t\t</single_bit>\n\t\t\t\t<double_bit>\n\t\t\t\t\t<device_memory>N/A</device_memory>\n\t\t\t\t\t<register_file>N/A</register_file>\n\t\t\t\t\t<l1_cache>N/A</l1_cache>\n\t\t\t\t\t<l2_cache>N/A</l2_cache>\n\t\t\t\t\t<texture_memory>N/A</texture_memory>\n\t\t\t\t\t<texture_shm>N/A</texture_shm>\n\t\t\t\t\t<cbu>N/A</cbu>\n\t\t\t\t\t<total>N/A</total>\n\t\t\t\t</double_bit>\n\t\t\t</volatile>\n\t\t\t<aggregate>\n\t\t\t\t<single_bit>\n\t\t\t\t\t<device_memory>N/A</device_memory>\n\t\t\t\t\t<register_file>N/A</register_file>\n\t\t\t\t\t<l1_cache>N/A</l1_cache>\n\t\t\t\t\t<l2_cache>N/A</l2_cache>\n\t\t\t\t\t<texture_memory>N/A</texture_memory>\n\t\t\t\t\t<texture_shm>N/A</texture_shm>\n\t\t\t\t\t<cbu>N/A</cbu>\n\t\t\t\t\t<total>N/A</total>\n\t\t\t\t</single_bit>\n\t\t\t\t<double_bit>\n\t\t\t\t\t<device_memory>N/A</device_memory>\n\t\t\t\t\t<register_file>N/A</register_file>\n\t\t\t\t\t<l1_cache>N/A</l1_cache>\n\t\t\t\t\t<l2_cache>N/A</l2_cache>\n\t\t\t\t\t<texture_memory>N/A</texture_memory>\n\t\t\t\t\t<texture_shm>N/A</texture_shm>\n\t\t\t\t\t<cbu>N/A</cbu>\n\t\t\t\t\t<total>N/A</total>\n\t\t\t\t</double_bit>\n\t\t\t</aggregate>\n\t\t</ecc_errors>\n\t\t<retired_pages>\n\t\t\t<multiple_single_bit_retirement>\n\t\t\t\t<retired_count>N/A</retired_count>\n\t\t\t\t<retired_pagelist>N/A</retired_pagelist>\n\t\t\t</multiple_single_bit_retirement>\n\t\t\t<double_bit_retirement>\n\t\t\t\t<retired_count>N/A</retired_count>\n\t\t\t\t<retired_pagelist>N/A</retired_pagelist>\n\t\t\t</double_bit_retirement>\n\t\t\t<pending_blacklist>N/A</pending_blacklist>\n\t\t\t<pending_retirement>N/A</pending_retirement>\n\t\t</retired_pages>\n\t\t<remapped_rows>N/A</remapped_rows>\n\t\t<temperature>\n\t\t\t<gpu_temp>34 C</gpu_temp>\n\t\t\t<gpu_temp_max_threshold>N/A</gpu_temp_max_threshold>\n\t\t\t<gpu_temp_slow_threshold>N/A</gpu_temp_slow_threshold>\n\t\t\t<gpu_temp_max_gpu_threshold>N/A</gpu_temp_max_gpu_threshold>\n\t\t\t<gpu_target_temperature>N/A</gpu_target_temperature>\n\t\t\t<memory_temp>N/A</memory_temp>\n\t\t\t<gpu_temp_max_mem_threshold>N/A</gpu_temp_max_mem_threshold>\n\t\t</temperature>\n\t\t<supported_gpu_target_temp>\n\t\t\t<gpu_target_temp_min>N/A</gpu_target_temp_min>\n\t\t\t<gpu_target_temp_max>N/A</gpu_target_temp_max>\n\t\t</supported_gpu_target_temp>\n\t\t<power_readings>\n\t\t\t<power_state>P0</power_state>\n\t\t\t<power_management>N/A</power_management>\n\t\t\t<power_draw>N/A</power_draw>\n\t\t\t<power_limit>N/A</power_limit>\n\t\t\t<default_power_limit>N/A</default_power_limit>\n\t\t\t<enforced_power_limit>N/A</enforced_power_limit>\n\t\t\t<min_power_limit>N/A</min_power_limit>\n\t\t\t<max_power_limit>N/A</max_power_limit>\n\t\t</power_readings>\n\t\t<clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t\t<sm_clock>N/A</sm_clock>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t\t<video_clock>N/A</video_clock>\n\t\t</clocks>\n\t\t<applications_clocks>\n\t\t\t<graphics_clock>901 MHz</graphics_clock>\n\t\t\t<mem_clock>800 MHz</mem_clock>\n\t\t</applications_clocks>\n\t\t<default_applications_clocks>\n\t\t\t<graphics_clock>901 MHz</graphics_clock>\n\t\t\t<mem_clock>800 MHz</mem_clock>\n\t\t</default_applications_clocks>\n\t\t<max_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t\t<sm_clock>N/A</sm_clock>\n\t\t\t<mem_clock>N/A</mem_clock>\n\t\t\t<video_clock>N/A</video_clock>\n\t\t</max_clocks>\n\t\t<max_customer_boost_clocks>\n\t\t\t<graphics_clock>N/A</graphics_clock>\n\t\t</max_customer_boost_clocks>\n\t\t<clock_policy>\n\t\t\t<auto_boost>N/A</auto_boost>\n\t\t\t<auto_boost_default>N/A</auto_boost_default>\n\t\t</clock_policy>\n\t\t<voltage>\n\t\t\t<graphics_volt>N/A</graphics_volt>\n\t\t</voltage>\n\t\t<supported_clocks>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>800 MHz</value>\n\t\t\t\t<supported_graphics_clock>901 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>888 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>875 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>862 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>849 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>836 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>823 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>810 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>797 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>784 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>771 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>758 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>745 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>732 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>719 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>705 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>692 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>679 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>666 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>653 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t\t<supported_mem_clock>\n\t\t\t\t<value>405 MHz</value>\n\t\t\t\t<supported_graphics_clock>405 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>402 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>400 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>398 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>396 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>394 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>392 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>390 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>388 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>386 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>384 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>382 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>380 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>378 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>376 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>374 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>372 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>370 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>368 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>366 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>364 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>361 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>359 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>357 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>355 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>353 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>351 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>349 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>347 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>345 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>343 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>341 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>339 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>337 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>335 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>333 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>331 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>329 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>327 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>325 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>323 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>321 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>318 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>316 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>314 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>312 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>310 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>308 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>306 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>304 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>302 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>300 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>298 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>296 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>294 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>292 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>290 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>288 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>286 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>284 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>282 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>280 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>278 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>275 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>273 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>271 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>269 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>267 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>265 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>263 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>261 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>259 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>257 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>255 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>253 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>251 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>249 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>247 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>245 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>243 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>241 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>239 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>237 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>235 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>232 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>230 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>228 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>226 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>224 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>222 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>220 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>218 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>216 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>214 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>212 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>210 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>208 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>206 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>204 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>202 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>200 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>198 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>196 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>194 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>192 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>189 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>187 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>185 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>183 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>181 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>179 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>177 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>175 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>173 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>171 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>169 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>167 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>165 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>163 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>161 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>159 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>157 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>155 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>153 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>151 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>149 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>146 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>144 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>142 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>140 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>138 MHz</supported_graphics_clock>\n\t\t\t\t<supported_graphics_clock>136 MHz</supported_graphics_clock>\n\t\t\t</supported_mem_clock>\n\t\t</supported_clocks>\n\t\t<processes>\n\t\t</processes>\n\t\t<accounted_processes>N/A</accounted_processes>\n\t</gpu>\n\n</nvidia_smi_log>\n"
  },
  {
    "path": "test/nvidia_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/nvdia.dart';\n\nconst _raw = '''\n<?xml version=\"1.0\" ?>\n<!DOCTYPE nvidia_smi_log SYSTEM \"nvsmi_device_v12.dtd\">\n<nvidia_smi_log>\n        <timestamp>Mon Nov 27 15:08:08 2023</timestamp>\n        <driver_version>535.129.03</driver_version>\n        <cuda_version>12.2</cuda_version>\n        <attached_gpus>1</attached_gpus>\n        <gpu id=\"00000000:01:00.0\">\n                <product_name>NVIDIA GeForce RTX 3080 Ti</product_name>\n                <product_brand>GeForce</product_brand>\n                <product_architecture>Ampere</product_architecture>\n                <display_mode>Enabled</display_mode>\n                <display_active>Enabled</display_active>\n                <persistence_mode>Disabled</persistence_mode>\n                <addressing_mode>None</addressing_mode>\n                <mig_mode>\n                        <current_mig>N/A</current_mig>\n                        <pending_mig>N/A</pending_mig>\n                </mig_mode>\n                <mig_devices>\n                        None\n                </mig_devices>\n                <accounting_mode>Disabled</accounting_mode>\n                <accounting_mode_buffer_size>4000</accounting_mode_buffer_size>\n                <driver_model>\n                        <current_dm>N/A</current_dm>\n                        <pending_dm>N/A</pending_dm>\n                </driver_model>\n                <serial>N/A</serial>\n                <uuid>GPU-71dd2db7-1b83-c2f0-8c13-46b94790bafd</uuid>\n                <minor_number>0</minor_number>\n                <vbios_version>94.02.71.00.28</vbios_version>\n                <multigpu_board>No</multigpu_board>\n                <board_id>0x100</board_id>\n                <board_part_number>N/A</board_part_number>\n                <gpu_part_number>2208-225-A1</gpu_part_number>\n                <gpu_fru_part_number>N/A</gpu_fru_part_number>\n                <gpu_module_id>1</gpu_module_id>\n                <inforom_version>\n                        <img_version>G001.0000.03.03</img_version>\n                        <oem_object>2.0</oem_object>\n                        <ecc_object>N/A</ecc_object>\n                        <pwr_object>N/A</pwr_object>\n                </inforom_version>\n                <inforom_bbx_flush>\n                        <latest_timestamp>N/A</latest_timestamp>\n                        <latest_duration>N/A</latest_duration>\n                </inforom_bbx_flush>\n                <gpu_operation_mode>\n                        <current_gom>N/A</current_gom>\n                        <pending_gom>N/A</pending_gom>\n                </gpu_operation_mode>\n                <gsp_firmware_version>N/A</gsp_firmware_version>\n                <gpu_virtualization_mode>\n                        <virtualization_mode>None</virtualization_mode>\n                        <host_vgpu_mode>N/A</host_vgpu_mode>\n                </gpu_virtualization_mode>\n                <gpu_reset_status>\n                        <reset_required>No</reset_required>\n                        <drain_and_reset_recommended>N/A</drain_and_reset_recommended>\n                </gpu_reset_status>\n                <ibmnpu>\n                        <relaxed_ordering_mode>N/A</relaxed_ordering_mode>\n                </ibmnpu>\n                <pci>\n                        <pci_bus>01</pci_bus>\n                        <pci_device>00</pci_device>\n                        <pci_domain>0000</pci_domain>\n                        <pci_device_id>220810DE</pci_device_id>\n                        <pci_bus_id>00000000:01:00.0</pci_bus_id>\n                        <pci_sub_system_id>15351B4C</pci_sub_system_id>\n                        <pci_gpu_link_info>\n                                <pcie_gen>\n                                        <max_link_gen>4</max_link_gen>\n                                        <current_link_gen>1</current_link_gen>\n                                        <device_current_link_gen>1</device_current_link_gen>\n                                        <max_device_link_gen>4</max_device_link_gen>\n                                        <max_host_link_gen>5</max_host_link_gen>\n                                </pcie_gen>\n                                <link_widths>\n                                        <max_link_width>16x</max_link_width>\n                                        <current_link_width>16x</current_link_width>\n                                </link_widths>\n                        </pci_gpu_link_info>\n                        <pci_bridge_chip>\n                                <bridge_chip_type>N/A</bridge_chip_type>\n                                <bridge_chip_fw>N/A</bridge_chip_fw>\n                        </pci_bridge_chip>\n                        <replay_counter>0</replay_counter>\n                        <replay_rollover_counter>0</replay_rollover_counter>\n                        <tx_util>0 KB/s</tx_util>\n                        <rx_util>0 KB/s</rx_util>\n                        <atomic_caps_inbound>N/A</atomic_caps_inbound>\n                        <atomic_caps_outbound>N/A</atomic_caps_outbound>\n                </pci>\n                <fan_speed>0 %</fan_speed>\n                <performance_state>P8</performance_state>\n                <clocks_event_reasons>\n                        <clocks_event_reason_gpu_idle>Active</clocks_event_reason_gpu_idle>\n                        <clocks_event_reason_applications_clocks_setting>Not Active</clocks_event_reason_applications_clocks_setting>\n                        <clocks_event_reason_sw_power_cap>Not Active</clocks_event_reason_sw_power_cap>\n                        <clocks_event_reason_hw_slowdown>Not Active</clocks_event_reason_hw_slowdown>\n                        <clocks_event_reason_hw_thermal_slowdown>Not Active</clocks_event_reason_hw_thermal_slowdown>\n                        <clocks_event_reason_hw_power_brake_slowdown>Not Active</clocks_event_reason_hw_power_brake_slowdown>\n                        <clocks_event_reason_sync_boost>Not Active</clocks_event_reason_sync_boost>\n                        <clocks_event_reason_sw_thermal_slowdown>Not Active</clocks_event_reason_sw_thermal_slowdown>\n                        <clocks_event_reason_display_clocks_setting>Not Active</clocks_event_reason_display_clocks_setting>\n                </clocks_event_reasons>\n                <fb_memory_usage>\n                        <total>12288 MiB</total>\n                        <reserved>245 MiB</reserved>\n                        <used>352 MiB</used>\n                        <free>11690 MiB</free>\n                </fb_memory_usage>\n                <bar1_memory_usage>\n                        <total>256 MiB</total>\n                        <used>7 MiB</used>\n                        <free>249 MiB</free>\n                </bar1_memory_usage>\n                <cc_protected_memory_usage>\n                        <total>0 MiB</total>\n                        <used>0 MiB</used>\n                        <free>0 MiB</free>\n                </cc_protected_memory_usage>\n                <compute_mode>Default</compute_mode>\n                <utilization>\n                        <gpu_util>3 %</gpu_util>\n                        <memory_util>2 %</memory_util>\n                        <encoder_util>0 %</encoder_util>\n                        <decoder_util>0 %</decoder_util>\n                        <jpeg_util>0 %</jpeg_util>\n                        <ofa_util>0 %</ofa_util>\n                </utilization>\n                <encoder_stats>\n                        <session_count>0</session_count>\n                        <average_fps>0</average_fps>\n                        <average_latency>0</average_latency>\n                </encoder_stats>\n                <fbc_stats>\n                        <session_count>0</session_count>\n                        <average_fps>0</average_fps>\n                        <average_latency>0</average_latency>\n                </fbc_stats>\n                <ecc_mode>\n                        <current_ecc>N/A</current_ecc>\n                        <pending_ecc>N/A</pending_ecc>\n                </ecc_mode>\n                <ecc_errors>\n                        <volatile>\n                                <sram_correctable>N/A</sram_correctable>\n                                <sram_uncorrectable>N/A</sram_uncorrectable>\n                                <dram_correctable>N/A</dram_correctable>\n                                <dram_uncorrectable>N/A</dram_uncorrectable>\n                        </volatile>\n                        <aggregate>\n                                <sram_correctable>N/A</sram_correctable>\n                                <sram_uncorrectable>N/A</sram_uncorrectable>\n                                <dram_correctable>N/A</dram_correctable>\n                                <dram_uncorrectable>N/A</dram_uncorrectable>\n                        </aggregate>\n                </ecc_errors>\n                <retired_pages>\n                        <multiple_single_bit_retirement>\n                                <retired_count>N/A</retired_count>\n                                <retired_pagelist>N/A</retired_pagelist>\n                        </multiple_single_bit_retirement>\n                        <double_bit_retirement>\n                                <retired_count>N/A</retired_count>\n                                <retired_pagelist>N/A</retired_pagelist>\n                        </double_bit_retirement>\n                        <pending_blacklist>N/A</pending_blacklist>\n                        <pending_retirement>N/A</pending_retirement>\n                </retired_pages>\n                <remapped_rows>N/A</remapped_rows>\n                <temperature>\n                        <gpu_temp>34 C</gpu_temp>\n                        <gpu_temp_tlimit>N/A</gpu_temp_tlimit>\n                        <gpu_temp_max_threshold>98 C</gpu_temp_max_threshold>\n                        <gpu_temp_slow_threshold>95 C</gpu_temp_slow_threshold>\n                        <gpu_temp_max_gpu_threshold>93 C</gpu_temp_max_gpu_threshold>\n                        <gpu_target_temperature>83 C</gpu_target_temperature>\n                        <memory_temp>N/A</memory_temp>\n                        <gpu_temp_max_mem_threshold>N/A</gpu_temp_max_mem_threshold>\n                </temperature>\n                <supported_gpu_target_temp>\n                        <gpu_target_temp_min>65 C</gpu_target_temp_min>\n                        <gpu_target_temp_max>91 C</gpu_target_temp_max>\n                </supported_gpu_target_temp>\n                <gpu_power_readings>\n                        <power_state>P8</power_state>\n                        <power_draw>24.55 W</power_draw>\n                        <current_power_limit>350.00 W</current_power_limit>\n                        <requested_power_limit>350.00 W</requested_power_limit>\n                        <default_power_limit>350.00 W</default_power_limit>\n                        <min_power_limit>100.00 W</min_power_limit>\n                        <max_power_limit>380.00 W</max_power_limit>\n                </gpu_power_readings>\n                <module_power_readings>\n                        <power_state>P8</power_state>\n                        <power_draw>N/A</power_draw>\n                        <current_power_limit>N/A</current_power_limit>\n                        <requested_power_limit>N/A</requested_power_limit>\n                        <default_power_limit>N/A</default_power_limit>\n                        <min_power_limit>N/A</min_power_limit>\n                        <max_power_limit>N/A</max_power_limit>\n                </module_power_readings>\n                <clocks>\n                        <graphics_clock>210 MHz</graphics_clock>\n                        <sm_clock>210 MHz</sm_clock>\n                        <mem_clock>405 MHz</mem_clock>\n                        <video_clock>555 MHz</video_clock>\n                </clocks>\n                <applications_clocks>\n                        <graphics_clock>N/A</graphics_clock>\n                        <mem_clock>N/A</mem_clock>\n                </applications_clocks>\n                <default_applications_clocks>\n                        <graphics_clock>N/A</graphics_clock>\n                        <mem_clock>N/A</mem_clock>\n                </default_applications_clocks>\n                <deferred_clocks>\n                        <mem_clock>N/A</mem_clock>\n                </deferred_clocks>\n                <max_clocks>\n                        <graphics_clock>2145 MHz</graphics_clock>\n                        <sm_clock>2145 MHz</sm_clock>\n                        <mem_clock>9501 MHz</mem_clock>\n                        <video_clock>1950 MHz</video_clock>\n                </max_clocks>\n                <max_customer_boost_clocks>\n                        <graphics_clock>N/A</graphics_clock>\n                </max_customer_boost_clocks>\n                <clock_policy>\n                        <auto_boost>N/A</auto_boost>\n                        <auto_boost_default>N/A</auto_boost_default>\n                </clock_policy>\n                <voltage>\n                        <graphics_volt>775.000 mV</graphics_volt>\n                </voltage>\n                <fabric>\n                        <state>N/A</state>\n                        <status>N/A</status>\n                </fabric>\n                <supported_clocks>\n                        <supported_mem_clock>\n                                <value>9501 MHz</value>\n                                <supported_graphics_clock>2145 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2130 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2115 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2100 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2085 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2070 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2055 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2040 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2025 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2010 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1995 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1980 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1965 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1950 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1935 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1920 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1905 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1890 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1875 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1860 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1845 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1830 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1815 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1800 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1785 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1770 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1755 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1740 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1725 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1710 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1695 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1680 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1665 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1650 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1635 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1620 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1605 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1590 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1575 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1560 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1545 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1530 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1515 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1500 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1485 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1470 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1455 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1440 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1425 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1410 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1395 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1380 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1365 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1350 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1335 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1320 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1305 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1290 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1275 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1260 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1245 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1230 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1215 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1200 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1185 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1170 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1155 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1140 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1125 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1110 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1095 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1080 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1065 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1050 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1035 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1020 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1005 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>990 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>975 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>960 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>945 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>930 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>915 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>900 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>885 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>870 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>855 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>840 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>825 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>810 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>795 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>780 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>765 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>750 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>735 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>720 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>705 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>690 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>675 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>660 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>645 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>630 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>615 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>600 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>585 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>570 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>555 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>540 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>525 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>510 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>495 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>480 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>465 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>450 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>435 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>420 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>405 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>390 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>375 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>360 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>345 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>330 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>315 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>300 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>285 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>270 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>255 MHz</supported_graphics_clock>\n                        </supported_mem_clock>\n                        <supported_mem_clock>\n                                <value>9251 MHz</value>\n                                <supported_graphics_clock>2145 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2130 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2115 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2100 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2085 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2070 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2055 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2040 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2025 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2010 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1995 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1980 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1965 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1950 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1935 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1920 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1905 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1890 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1875 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1860 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1845 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1830 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1815 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1800 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1785 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1770 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1755 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1740 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1725 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1710 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1695 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1680 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1665 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1650 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1635 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1620 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1605 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1590 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1575 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1560 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1545 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1530 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1515 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1500 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1485 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1470 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1455 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1440 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1425 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1410 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1395 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1380 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1365 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1350 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1335 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1320 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1305 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1290 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1275 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1260 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1245 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1230 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1215 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1200 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1185 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1170 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1155 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1140 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1125 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1110 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1095 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1080 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1065 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1050 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1035 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1020 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1005 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>990 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>975 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>960 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>945 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>930 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>915 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>900 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>885 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>870 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>855 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>840 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>825 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>810 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>795 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>780 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>765 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>750 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>735 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>720 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>705 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>690 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>675 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>660 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>645 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>630 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>615 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>600 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>585 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>570 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>555 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>540 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>525 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>510 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>495 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>480 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>465 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>450 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>435 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>420 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>405 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>390 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>375 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>360 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>345 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>330 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>315 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>300 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>285 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>270 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>255 MHz</supported_graphics_clock>\n                        </supported_mem_clock>\n                        <supported_mem_clock>\n                                <value>5001 MHz</value>\n                                <supported_graphics_clock>2145 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2130 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2115 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2100 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2085 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2070 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2055 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2040 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2025 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2010 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1995 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1980 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1965 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1950 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1935 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1920 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1905 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1890 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1875 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1860 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1845 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1830 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1815 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1800 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1785 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1770 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1755 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1740 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1725 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1710 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1695 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1680 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1665 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1650 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1635 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1620 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1605 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1590 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1575 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1560 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1545 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1530 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1515 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1500 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1485 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1470 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1455 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1440 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1425 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1410 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1395 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1380 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1365 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1350 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1335 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1320 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1305 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1290 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1275 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1260 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1245 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1230 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1215 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1200 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1185 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1170 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1155 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1140 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1125 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1110 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1095 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1080 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1065 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1050 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1035 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1020 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1005 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>990 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>975 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>960 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>945 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>930 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>915 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>900 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>885 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>870 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>855 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>840 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>825 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>810 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>795 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>780 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>765 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>750 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>735 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>720 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>705 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>690 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>675 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>660 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>645 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>630 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>615 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>600 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>585 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>570 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>555 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>540 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>525 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>510 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>495 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>480 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>465 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>450 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>435 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>420 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>405 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>390 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>375 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>360 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>345 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>330 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>315 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>300 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>285 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>270 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>255 MHz</supported_graphics_clock>\n                        </supported_mem_clock>\n                        <supported_mem_clock>\n                                <value>810 MHz</value>\n                                <supported_graphics_clock>2100 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2085 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2070 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2055 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2040 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2025 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>2010 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1995 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1980 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1965 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1950 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1935 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1920 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1905 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1890 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1875 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1860 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1845 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1830 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1815 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1800 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1785 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1770 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1755 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1740 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1725 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1710 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1695 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1680 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1665 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1650 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1635 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1620 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1605 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1590 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1575 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1560 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1545 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1530 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1515 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1500 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1485 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1470 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1455 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1440 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1425 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1410 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1395 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1380 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1365 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1350 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1335 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1320 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1305 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1290 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1275 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1260 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1245 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1230 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1215 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1200 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1185 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1170 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1155 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1140 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1125 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1110 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1095 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1080 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1065 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1050 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1035 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1020 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>1005 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>990 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>975 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>960 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>945 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>930 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>915 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>900 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>885 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>870 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>855 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>840 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>825 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>810 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>795 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>780 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>765 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>750 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>735 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>720 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>705 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>690 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>675 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>660 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>645 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>630 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>615 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>600 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>585 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>570 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>555 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>540 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>525 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>510 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>495 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>480 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>465 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>450 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>435 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>420 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>405 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>390 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>375 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>360 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>345 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>330 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>315 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>300 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>285 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>270 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>255 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>240 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>225 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>210 MHz</supported_graphics_clock>\n                        </supported_mem_clock>\n                        <supported_mem_clock>\n                                <value>405 MHz</value>\n                                <supported_graphics_clock>420 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>405 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>390 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>375 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>360 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>345 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>330 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>315 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>300 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>285 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>270 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>255 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>240 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>225 MHz</supported_graphics_clock>\n                                <supported_graphics_clock>210 MHz</supported_graphics_clock>\n                        </supported_mem_clock>\n                </supported_clocks>\n                <processes>\n                        <process_info>\n                                <gpu_instance_id>N/A</gpu_instance_id>\n                                <compute_instance_id>N/A</compute_instance_id>\n                                <pid>1575</pid>\n                                <type>G</type>\n                                <process_name>/usr/lib/xorg/Xorg</process_name>\n                                <used_memory>220 MiB</used_memory>\n                        </process_info>\n                        <process_info>\n                                <gpu_instance_id>N/A</gpu_instance_id>\n                                <compute_instance_id>N/A</compute_instance_id>\n                                <pid>1933</pid>\n                                <type>G</type>\n                                <process_name>/usr/bin/gnome-shell</process_name>\n                                <used_memory>34 MiB</used_memory>\n                        </process_info>\n                        <process_info>\n                                <gpu_instance_id>N/A</gpu_instance_id>\n                                <compute_instance_id>N/A</compute_instance_id>\n                                <pid>16484</pid>\n                                <type>G</type>\n                                <process_name>/opt/microsoft/msedge/msedge --type=gpu-process --crashpad-handler-pid=16445 --enable-crash-reporter=, --change-stack-guard-on-fork=enable --gpu-preferences=WAAAAAAAAAAgAAAEAAAAAAAAAAAAAAAAAABgAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAGAAAAAAAAAAYAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAIAAAAAAAAAA== --shared-files --field-trial-handle=0,i,8049841253453661893,8316967731219016996,262144</process_name>\n                                <used_memory>76 MiB</used_memory>\n                        </process_info>\n                </processes>\n                <accounted_processes>\n                </accounted_processes>\n        </gpu>\n\n</nvidia_smi_log>\n''';\n\n/// [\n///   {\n///     \"name\": \"GeForce RTX 3090\",\n///     \"temp\": 40,\n///     \"power\": \"30W / 350W\",\n///     \"memory\": {\n///       \"total\": 24268,\n///       \"used\": 240,\n///       \"unit\": \"MiB\",\n///       \"processes\": [\n///         {\n///           \"pid\": 1456,\n///           \"name\": \"/usr/lib/xorg/Xorg\",\n///           \"memory\": 40\n///         },\n///       ]\n///     },\n///   }\n/// ]\n\nvoid main() {\n  test('nvdia-smi', () {\n    final items = NvidiaSmi.fromXml(_raw);\n    expect(items.length, 1);\n    final item = items[0];\n    expect(item.name, 'NVIDIA GeForce RTX 3080 Ti');\n    expect(item.temp, 34);\n    expect(item.power, '24.55 W / 350.00 W');\n    expect(item.memory.total, 12288);\n    expect(item.memory.used, 352);\n    expect(item.memory.unit, 'MiB');\n    final processes = item.memory.processes;\n    expect(processes.length, 3);\n    expect(processes[0].pid, 1575);\n    expect(processes[0].name, '/usr/lib/xorg/Xorg');\n    expect(processes[0].memory, 220);\n    expect(processes[1].pid, 1933);\n    expect(processes[1].name, '/usr/bin/gnome-shell');\n    expect(processes[1].memory, 34);\n    expect(processes[2].pid, 16484);\n    expect(processes[2].memory, 76);\n  });\n\n  test('nvidia-smi with N/A', () async {\n    final raw = await File('test/nvidia.xml').readAsString();\n    final items = NvidiaSmi.fromXml(raw);\n    expect(items.length, 4);\n  });\n\n  test('nvidia-smi 2', () async {\n    final raw = await File('test/nvidia2.xml').readAsString();\n    final items = NvidiaSmi.fromXml(raw);\n    expect(items.length, 1);\n  });\n}\n"
  },
  {
    "path": "test/proc_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/proc.dart';\n\nvoid main() {\n  test('parse process', () {\n    const raw = '''\n  PID USER       VSZ STAT COMMAND\n    1 root      1276 S    /sbin/procd\n''';\n    final psResult = PsResult.parse(raw);\n    expect(psResult.procs.length, 1);\n  });\n}\n"
  },
  {
    "path": "test/pve_test.dart",
    "content": "import 'dart:convert';\n\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/pve.dart';\n\nconst _raw = '''\n{\n    \"data\": [\n        {\n            \"maxmem\": 12884901888,\n            \"type\": \"lxc\",\n            \"cpu\": 0.0544631947461575,\n            \"netin\": 65412250538,\n            \"template\": 0,\n            \"diskread\": 324033204224,\n            \"maxcpu\": 8,\n            \"disk\": 29767077888,\n            \"diskwrite\": 707866570752,\n            \"node\": \"pve\",\n            \"vmid\": 100,\n            \"mem\": 5389254656,\n            \"status\": \"running\",\n            \"netout\": 66898114418,\n            \"uptime\": 1204757,\n            \"id\": \"lxc/100\",\n            \"maxdisk\": 134145380352,\n            \"name\": \"Jellyfin\"\n        },\n        {\n            \"vmid\": 101,\n            \"node\": \"pve\",\n            \"uptime\": 0,\n            \"netout\": 0,\n            \"status\": \"stopped\",\n            \"mem\": 0,\n            \"id\": \"qemu/101\",\n            \"name\": \"ubuntu\",\n            \"maxdisk\": 137438953472,\n            \"maxmem\": 6442450944,\n            \"cpu\": 0,\n            \"netin\": 0,\n            \"type\": \"qemu\",\n            \"disk\": 0,\n            \"diskread\": 0,\n            \"template\": 0,\n            \"maxcpu\": 8,\n            \"diskwrite\": 0\n        },\n        {\n            \"maxcpu\": 4,\n            \"template\": 0,\n            \"diskread\": 23287297536,\n            \"disk\": 0,\n            \"diskwrite\": 39555984896,\n            \"maxmem\": 4294967296,\n            \"type\": \"qemu\",\n            \"netin\": 2190678599,\n            \"cpu\": 0.0516426831961466,\n            \"id\": \"qemu/102\",\n            \"maxdisk\": 0,\n            \"name\": \"win\",\n            \"node\": \"pve\",\n            \"vmid\": 102,\n            \"mem\": 1791827968,\n            \"status\": \"running\",\n            \"netout\": 213292068,\n            \"uptime\": 1013075\n        },\n        {\n            \"maxcpu\": 12,\n            \"id\": \"node/pve\",\n            \"disk\": 358415503360,\n            \"maxdisk\": 998011547648,\n            \"cgroup-mode\": 2,\n            \"node\": \"pve\",\n            \"maxmem\": 29287632896,\n            \"type\": \"node\",\n            \"status\": \"online\",\n            \"mem\": 11522887680,\n            \"cpu\": 0.0451634094268353,\n            \"level\": \"\",\n            \"uptime\": 1204771\n        },\n        {\n            \"id\": \"storage/pve/DSM\",\n            \"disk\": 1250082226176,\n            \"maxdisk\": 9909187887104,\n            \"storage\": \"DSM\",\n            \"node\": \"pve\",\n            \"status\": \"available\",\n            \"type\": \"storage\",\n            \"plugintype\": \"cifs\",\n            \"content\": \"snippets,backup,images,rootdir,vztmpl,iso\",\n            \"shared\": 1\n        },\n        {\n            \"type\": \"storage\",\n            \"status\": \"available\",\n            \"plugintype\": \"dir\",\n            \"content\": \"iso,vztmpl,images,rootdir,backup,snippets\",\n            \"shared\": 0,\n            \"node\": \"pve\",\n            \"maxdisk\": 1967847137280,\n            \"storage\": \"hard\",\n            \"id\": \"storage/pve/hard\",\n            \"disk\": 620950544384\n        },\n        {\n            \"maxdisk\": 998011547648,\n            \"storage\": \"local\",\n            \"disk\": 358415503360,\n            \"id\": \"storage/pve/local\",\n            \"status\": \"available\",\n            \"type\": \"storage\",\n            \"plugintype\": \"dir\",\n            \"content\": \"backup,snippets,rootdir,images,vztmpl,iso\",\n            \"shared\": 0,\n            \"node\": \"pve\"\n        },\n        {\n            \"id\": \"sdn/pve/localnetwork\",\n            \"node\": \"pve\",\n            \"sdn\": \"localnetwork\",\n            \"status\": \"ok\",\n            \"type\": \"sdn\"\n        }\n    ]\n}''';\n\nvoid main() {\n  test('parse pve', () {\n    final list = json.decode(_raw)['data'] as List;\n    final pveItems = list.map((e) => PveResIface.fromJson(e)).toList();\n    expect(pveItems.length, 8);\n  });\n}\n"
  },
  {
    "path": "test/script_builder_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/app/scripts/script_builders.dart';\nimport 'package:server_box/data/model/app/scripts/script_consts.dart';\nimport 'package:server_box/data/model/app/scripts/shell_func.dart';\nimport 'package:server_box/data/model/server/system.dart';\n\nvoid main() {\n  group('Script Builder Integration Tests', () {\n    test('script generation produces valid output for all platforms', () {\n      for (final builder in ScriptBuilderFactory.getAllBuilders()) {\n        final script = builder.buildScript(null);\n\n        // Basic validation\n        expect(script, isNotEmpty, reason: 'Script should not be empty for ${builder.runtimeType}');\n\n        // Should contain all required functions\n        for (final func in ShellFunc.values) {\n          expect(script, contains(func.name), reason: 'Script should contain function ${func.name}');\n        }\n\n        // Should contain proper headers\n        expect(script, contains(builder.scriptHeader), reason: 'Script should contain proper header');\n\n        // Should be well-formed\n        if (builder is WindowsScriptBuilder) {\n          expect(\n            script,\n            contains('switch (\\$args[0])'),\n            reason: 'Windows script should contain switch statement',\n          );\n          expect(\n            script,\n            contains('PowerShell script for ServerBox'),\n            reason: 'Windows script should have PowerShell header',\n          );\n        } else if (builder is UnixScriptBuilder) {\n          expect(script, contains('#!/bin/sh'), reason: 'Unix script should have shebang');\n          expect(script, contains('case \\$1 in'), reason: 'Unix script should contain case statement');\n        }\n      }\n    });\n\n    test('script generation with custom commands works correctly', () {\n      final customCmds = {'custom_test': 'echo \"Custom test command\"', 'another_cmd': 'whoami'};\n\n      for (final builder in ScriptBuilderFactory.getAllBuilders()) {\n        final script = builder.buildScript(customCmds);\n\n        expect(script, isNotEmpty);\n\n        // Custom commands should only be included in status function for both platforms\n        if (builder is UnixScriptBuilder) {\n          expect(script, contains('echo \"Custom test command\"'));\n          expect(script, contains('whoami'));\n        }\n\n        // Windows builder should include custom commands in SbStatus function\n        if (builder is WindowsScriptBuilder) {\n          expect(script, contains('echo \"Custom test command\"'));\n          expect(script, contains('whoami'));\n        }\n      }\n    });\n\n    test('script file names are correct for each platform', () {\n      final windowsBuilder = ScriptBuilderFactory.getBuilder(true);\n      final unixBuilder = ScriptBuilderFactory.getBuilder(false);\n\n      expect(windowsBuilder.scriptFileName, equals(ScriptConstants.scriptFileWindows));\n      expect(windowsBuilder.scriptFileName, endsWith('.ps1'));\n\n      expect(unixBuilder.scriptFileName, equals(ScriptConstants.scriptFile));\n      expect(unixBuilder.scriptFileName, endsWith('.sh'));\n    });\n\n    test('install commands are generated correctly', () {\n      const testDir = '/tmp/test';\n      const testPath = '/tmp/test/script.sh';\n\n      final unixBuilder = ScriptBuilderFactory.getBuilder(false);\n      final installCmd = unixBuilder.getInstallCommand(testDir, testPath);\n\n      expect(installCmd, contains('mkdir'));\n      expect(installCmd, contains('chmod 755'));\n      expect(installCmd, contains(testPath));\n\n      const testDirWindows = 'C:\\\\temp\\\\test';\n      const testPathWindows = 'C:\\\\temp\\\\test\\\\script.ps1';\n\n      final windowsBuilder = ScriptBuilderFactory.getBuilder(true);\n      final installCmdWindows = windowsBuilder.getInstallCommand(testDirWindows, testPathWindows);\n\n      expect(installCmdWindows, contains('New-Item'));\n      expect(installCmdWindows, contains('Set-Content'));\n      expect(installCmdWindows, contains(testPathWindows));\n    });\n\n    test('exec commands are generated correctly for all platforms', () {\n      const testPath = '/tmp/test/script.sh';\n      const testPathWindows = 'C:\\\\temp\\\\test\\\\script.ps1';\n\n      final unixBuilder = ScriptBuilderFactory.getBuilder(false);\n      final windowsBuilder = ScriptBuilderFactory.getBuilder(true);\n\n      for (final func in ShellFunc.values) {\n        final unixExec = unixBuilder.getExecCommand(testPath, func);\n        expect(unixExec, contains(testPath));\n        expect(unixExec, contains(func.flag));\n\n        final windowsExec = windowsBuilder.getExecCommand(testPathWindows, func);\n        expect(windowsExec, contains('powershell'));\n        expect(windowsExec, contains('-ExecutionPolicy Bypass'));\n        expect(windowsExec, contains(func.flag));\n      }\n    });\n\n    test('script headers contain proper metadata', () {\n      final windowsBuilder = ScriptBuilderFactory.getBuilder(true);\n      final unixBuilder = ScriptBuilderFactory.getBuilder(false);\n\n      expect(windowsBuilder.scriptHeader, contains('PowerShell script for ServerBox'));\n      expect(windowsBuilder.scriptHeader, contains('DO NOT delete this file'));\n      expect(windowsBuilder.scriptHeader, contains('\\$ErrorActionPreference = \"SilentlyContinue\"'));\n\n      expect(unixBuilder.scriptHeader, contains('#!/bin/sh'));\n      expect(unixBuilder.scriptHeader, contains('Script for ServerBox'));\n      expect(unixBuilder.scriptHeader, contains('DO NOT delete this file'));\n      expect(unixBuilder.scriptHeader, contains('export LANG=en_US.UTF-8'));\n    });\n\n    test('scripts handle all system types properly', () {\n      // Test that system type detection is properly handled\n      final unixScript = ShellFuncManager.allScript(null, systemType: SystemType.linux);\n      final bsdScript = ShellFuncManager.allScript(null, systemType: SystemType.bsd);\n      final windowsScript = ShellFuncManager.allScript(null, systemType: SystemType.windows);\n\n      expect(unixScript, contains('#!/bin/sh'));\n      expect(bsdScript, contains('#!/bin/sh')); // BSD uses same script as Linux\n      expect(windowsScript, contains('PowerShell script'));\n\n      // Verify OS detection logic in Unix script\n      expect(unixScript, contains('macSign='));\n      expect(unixScript, contains('bsdSign='));\n      expect(unixScript, contains('isBusybox='));\n    });\n\n    test('error handling in script generation', () {\n      // Test that script generation doesn't throw with edge cases\n      expect(() => ShellFuncManager.allScript({}, systemType: SystemType.linux), returnsNormally);\n      expect(() => ShellFuncManager.allScript(null, systemType: SystemType.windows), returnsNormally);\n\n      // Test with empty custom commands\n      expect(() => ShellFuncManager.allScript({}, systemType: SystemType.bsd), returnsNormally);\n\n      // Test with null system type (should default to something)\n      expect(() => ShellFuncManager.allScript(null), returnsNormally);\n    });\n  });\n\n  group('ScriptBuilderFactory Tests', () {\n    test('factory returns correct builder types', () {\n      final windowsBuilder = ScriptBuilderFactory.getBuilder(true);\n      final unixBuilder = ScriptBuilderFactory.getBuilder(false);\n\n      expect(windowsBuilder, isA<WindowsScriptBuilder>());\n      expect(unixBuilder, isA<UnixScriptBuilder>());\n    });\n\n    test('getAllBuilders returns all available builders', () {\n      final builders = ScriptBuilderFactory.getAllBuilders();\n\n      expect(builders, hasLength(2));\n      expect(builders.any((b) => b is WindowsScriptBuilder), isTrue);\n      expect(builders.any((b) => b is UnixScriptBuilder), isTrue);\n    });\n  });\n}\n"
  },
  {
    "path": "test/sensors_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/sensors.dart';\n\nconst _sensorsRaw = '''\ncoretemp-isa-0000\nAdapter: ISA adapter\nPackage id 0:  +56.0°C  (high = +105.0°C, crit = +105.0°C)\nCore 0:        +45.0°C  (high = +105.0°C, crit = +105.0°C)\nCore 1:        +45.0°C  (high = +105.0°C, crit = +105.0°C)\nCore 2:        +45.0°C  (high = +105.0°C, crit = +105.0°C)\nCore 3:        +44.0°C  (high = +105.0°C, crit = +105.0°C)\n\nacpitz-acpi-0\nAdapter: ACPI interface\ntemp1:        +27.8°C  (crit = +119.0°C)\n\niwlwifi_1-virtual-0\nAdapter: Virtual device\ntemp1:        +56.0°C  \n\nnvme-pci-0400\nAdapter: PCI adapter\nComposite:    +45.9°C  (low  = -273.1°C, high = +83.8°C)\n                       (crit = +84.8°C)\nSensor 1:     +45.9°C  (low  = -273.1°C, high = +65261.8°C)\nSensor 2:     +47.9°C  (low  = -273.1°C, high = +65261.8°C)\n''';\n\nconst _sensorsRaw2 = '''\nasusec-isa-0000\nAdapter: ISA adapter\nCPU Core:      1.26 V\nChipset:     2473 RPM\nChipset:      +60.0°C\nCPU:          +36.0°C\nMotherboard:  +34.0°C\nT_Sensor:     -40.0°C\nVRM:          +20.0°C\nCPU:          35.00 A\n\nnct6798-isa-0290\nAdapter: ISA adapter\nin0:                        1.19 V  (min =  +0.00 V, max =  +1.74 V)\nin1:                      1000.00 mV (min =  +0.00 V, max =  +0.00 V)  ALARM\nin2:                        3.34 V  (min =  +0.00 V, max =  +0.00 V)  ALARM\nin3:                        3.28 V  (min =  +0.00 V, max =  +0.00 V)  ALARM\nin4:                      1000.00 mV (min =  +0.00 V, max =  +0.00 V)  ALARM\nin5:                      856.00 mV (min =  +0.00 V, max =  +0.00 V)\nin6:                      232.00 mV (min =  +0.00 V, max =  +0.00 V)  ALARM\nin7:                        3.34 V  (min =  +0.00 V, max =  +0.00 V)  ALARM\nin8:                        3.23 V  (min =  +0.00 V, max =  +0.00 V)  ALARM\nin9:                        1.78 V  (min =  +0.00 V, max =  +0.00 V)  ALARM\nin10:                     848.00 mV (min =  +0.00 V, max =  +0.00 V)  ALARM\nin11:                     880.00 mV (min =  +0.00 V, max =  +0.00 V)  ALARM\nin12:                       1.03 V  (min =  +0.00 V, max =  +0.00 V)  ALARM\nin13:                     320.00 mV (min =  +0.00 V, max =  +0.00 V)  ALARM\nin14:                     240.00 mV (min =  +0.00 V, max =  +0.00 V)  ALARM\nfan1:                        0 RPM  (min =    0 RPM)\nfan2:                     1764 RPM  (min =    0 RPM)\nfan3:                        0 RPM  (min =    0 RPM)\nfan4:                        0 RPM  (min =    0 RPM)\nfan5:                        0 RPM  (min =    0 RPM)\nfan6:                        0 RPM  (min =    0 RPM)\nSYSTIN:                    +34.0°C  (high = +80.0°C, hyst = +75.0°C)\n                                    (crit = +125.0°C)  sensor = thermistor\nCPUTIN:                    +35.0°C  (high = +80.0°C, hyst = +75.0°C)\n                                    (crit = +125.0°C)  sensor = thermistor\nAUXTIN0:                   +90.0°C  (high = +80.0°C, hyst = +75.0°C)  ALARM\n                                    (crit = +125.0°C)  sensor = thermistor\nAUXTIN1:                   +34.0°C  (high = +80.0°C, hyst = +75.0°C)\n                                    (crit = +125.0°C)  sensor = thermistor\nAUXTIN2:                   +33.0°C  (high = +80.0°C, hyst = +75.0°C)\n                                    (crit = +100.0°C)  sensor = thermistor\nAUXTIN3:                   +95.0°C  (high = +80.0°C, hyst = +75.0°C)  ALARM\n                                    (crit = +100.0°C)  sensor = thermistor\nAUXTIN4:                   +34.0°C  (high = +80.0°C, hyst = +75.0°C)\n                                    (crit = +100.0°C)\nPECI Agent 0 Calibration:  +36.0°C  (high = +80.0°C, hyst = +75.0°C)\nPCH_CHIP_CPU_MAX_TEMP:      +0.0°C\nPCH_CHIP_TEMP:              +0.0°C\nPCH_CPU_TEMP:               +0.0°C\nPCH_MCH_TEMP:               +0.0°C\nTSI0_TEMP:                 +44.6°C\nTSI1_TEMP:                 +60.0°C\nintrusion0:               ALARM\nintrusion1:               ALARM\nbeep_enable:              disabled\n\nnvme-pci-0400\nAdapter: PCI adapter\nComposite:    +45.9°C  (low  = -273.1°C, high = +69.8°C)\n                       (crit = +79.8°C)\n\nk10temp-pci-00c3\nAdapter: PCI adapter\nTctl:         +44.9°C\nTccd1:        +41.0°C\nTccd2:        +38.5°C\n''';\n\nvoid main() {\n  test('parse sensors1', () {\n    final sensors = SensorItem.parse(_sensorsRaw);\n    expect(sensors.map((e) => e.device), [\n      'coretemp-isa-0000',\n      'acpitz-acpi-0',\n      'iwlwifi_1-virtual-0',\n      'nvme-pci-0400',\n    ]);\n    expect(sensors.map((e) => e.adapter), [\n      SensorAdaptor.isa,\n      SensorAdaptor.acpi,\n      SensorAdaptor.virtual,\n      SensorAdaptor.pci,\n    ]);\n    expect(sensors.map((e) => e.summary), [\n      '+56.0°C  (high = +105.0°C, crit = +105.0°C)',\n      '+27.8°C  (crit = +119.0°C)',\n      '+56.0°C',\n      '+45.9°C  (low  = -273.1°C, high = +83.8°C)',\n    ]);\n  });\n\n  test('parse sensors2', () {\n    final sensors = SensorItem.parse(_sensorsRaw2);\n    expect(sensors.map((e) => e.device), [\n      'asusec-isa-0000',\n      'nct6798-isa-0290',\n      'nvme-pci-0400',\n      'k10temp-pci-00c3',\n    ]);\n    expect(sensors.map((e) => e.adapter), [\n      SensorAdaptor.isa,\n      SensorAdaptor.isa,\n      SensorAdaptor.pci,\n      SensorAdaptor.pci,\n    ]);\n    expect(sensors.map((e) => e.summary), [\n      '1.26 V',\n      '1.19 V  (min =  +0.00 V, max =  +1.74 V)',\n      '+45.9°C  (low  = -273.1°C, high = +69.8°C)',\n      '+44.9°C',\n    ]);\n  });\n}\n"
  },
  {
    "path": "test/server_dedup_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/core/utils/server_dedup.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\n\n// Mock functions to test the deduplication logic without relying on ServerStore\nList<Spi> _mockDeduplicateServers(List<Spi> importedServers, List<Spi> existingServers) {\n  final deduplicated = <Spi>[];\n  \n  for (final imported in importedServers) {\n    // Check against existing servers\n    if (!_mockIsDuplicate(imported, existingServers)) {\n      // Also check against already processed imported servers\n      if (!_mockIsDuplicate(imported, deduplicated)) {\n        deduplicated.add(imported);\n      }\n    }\n  }\n  \n  return deduplicated;\n}\n\nbool _mockIsDuplicate(Spi imported, List<Spi> existing) {\n  for (final existingSpi in existing) {\n    // Check for exact match on ip:port@user combination\n    if (existingSpi.ip == imported.ip && \n        existingSpi.port == imported.port && \n        existingSpi.user == imported.user) {\n      return true;\n    }\n  }\n  \n  return false;\n}\n\nList<Spi> _mockResolveNameConflicts(List<Spi> importedServers, List<String> existingNames) {\n  final existingNamesSet = existingNames.toSet();\n  final processedNames = <String>{};\n  final result = <Spi>[];\n  \n  for (final server in importedServers) {\n    String newName = server.name;\n    int suffix = 2;\n    \n    // Check against both existing servers and already processed servers\n    while (existingNamesSet.contains(newName) || processedNames.contains(newName)) {\n      newName = '${server.name} ($suffix)';\n      suffix++;\n    }\n    \n    processedNames.add(newName);\n    \n    if (newName != server.name) {\n      result.add(server.copyWith(name: newName));\n    } else {\n      result.add(server);\n    }\n  }\n  \n  return result;\n}\n\nvoid main() {\n  group('ServerDeduplication Tests', () {\n    late List<Spi> existingServers;\n    late List<Spi> importedServers;\n\n    setUp(() {\n      // Set up some existing servers for testing\n      existingServers = [\n        const Spi(\n          name: 'production-web',\n          ip: '192.168.1.100',\n          port: 22,\n          user: 'root',\n          id: 'existing1',\n        ),\n        const Spi(\n          name: 'staging-db',\n          ip: '192.168.1.200',\n          port: 22,\n          user: 'postgres',\n          id: 'existing2',\n        ),\n        const Spi(\n          name: 'dev-server',\n          ip: '192.168.1.50',\n          port: 2222,\n          user: 'developer',\n          id: 'existing3',\n        ),\n      ];\n    });\n\n    test('deduplicateServers removes exact duplicates', () {\n      importedServers = [\n        const Spi(\n          name: 'new-server-1',\n          ip: '192.168.1.100',\n          port: 22,\n          user: 'root', // Same as existing1\n        ),\n        const Spi(\n          name: 'new-server-2',\n          ip: '192.168.1.300',\n          port: 22,\n          user: 'admin', // New server\n        ),\n        const Spi(\n          name: 'new-server-3',\n          ip: '192.168.1.200',\n          port: 22,\n          user: 'postgres', // Same as existing2\n        ),\n      ];\n\n      final deduplicated = _mockDeduplicateServers(importedServers, existingServers);\n\n      expect(deduplicated, hasLength(1));\n      expect(deduplicated.first.name, 'new-server-2');\n      expect(deduplicated.first.ip, '192.168.1.300');\n    });\n\n    test('deduplicateServers considers port and user in deduplication', () {\n      importedServers = [\n        const Spi(\n          name: 'same-ip-diff-port',\n          ip: '192.168.1.100',\n          port: 2222, // Different port\n          user: 'root',\n        ),\n        const Spi(\n          name: 'same-ip-diff-user',\n          ip: '192.168.1.100',\n          port: 22,\n          user: 'admin', // Different user\n        ),\n        const Spi(\n          name: 'exact-duplicate',\n          ip: '192.168.1.100',\n          port: 22,\n          user: 'root', // Exact duplicate\n        ),\n      ];\n\n      final deduplicated = _mockDeduplicateServers(importedServers, existingServers);\n\n      expect(deduplicated, hasLength(2));\n      expect(deduplicated.any((s) => s.name == 'same-ip-diff-port'), isTrue);\n      expect(deduplicated.any((s) => s.name == 'same-ip-diff-user'), isTrue);\n      expect(deduplicated.any((s) => s.name == 'exact-duplicate'), isFalse);\n    });\n\n    test('deduplicateServers handles empty existing servers list', () {\n      importedServers = [\n        const Spi(\n          name: 'server1',\n          ip: '192.168.1.100',\n          port: 22,\n          user: 'root',\n        ),\n        const Spi(\n          name: 'server2',\n          ip: '192.168.1.200',\n          port: 22,\n          user: 'admin',\n        ),\n      ];\n\n      final deduplicated = _mockDeduplicateServers(importedServers, []);\n\n      expect(deduplicated, hasLength(2));\n      expect(deduplicated, equals(importedServers));\n    });\n\n    test('deduplicateServers handles empty imported servers list', () {\n      final deduplicated = _mockDeduplicateServers([], existingServers);\n\n      expect(deduplicated, isEmpty);\n    });\n\n    test('resolveNameConflicts appends numbers to conflicting names', () {\n      importedServers = [\n        const Spi(\n          name: 'production-web', // Conflicts with existing\n          ip: '192.168.1.300',\n          port: 22,\n          user: 'root',\n        ),\n        const Spi(\n          name: 'dev-server', // Conflicts with existing\n          ip: '192.168.1.400',\n          port: 22,\n          user: 'root',\n        ),\n        const Spi(\n          name: 'unique-name', // No conflict\n          ip: '192.168.1.500',\n          port: 22,\n          user: 'root',\n        ),\n      ];\n\n      final resolved = _mockResolveNameConflicts(\n        importedServers,\n        existingServers.map((s) => s.name).toList(),\n      );\n\n      expect(resolved, hasLength(3));\n      expect(resolved[0].name, 'production-web (2)');\n      expect(resolved[1].name, 'dev-server (2)');\n      expect(resolved[2].name, 'unique-name');\n    });\n\n    test('resolveNameConflicts handles multiple conflicts with same base name', () {\n      importedServers = [\n        const Spi(\n          name: 'server',\n          ip: '192.168.1.100',\n          port: 22,\n          user: 'root',\n        ),\n        const Spi(\n          name: 'server',\n          ip: '192.168.1.200',\n          port: 22,\n          user: 'admin',\n        ),\n        const Spi(\n          name: 'server',\n          ip: '192.168.1.300',\n          port: 2222,\n          user: 'root',\n        ),\n      ];\n\n      final existingNames = ['server', 'server (2)'];\n      final resolved = _mockResolveNameConflicts(importedServers, existingNames);\n\n      expect(resolved, hasLength(3));\n      expect(resolved[0].name, 'server (3)');\n      expect(resolved[1].name, 'server (4)');\n      expect(resolved[2].name, 'server (5)');\n    });\n\n    test('resolveNameConflicts handles empty input', () {\n      final resolved = _mockResolveNameConflicts([], ['existing1', 'existing2']);\n\n      expect(resolved, isEmpty);\n    });\n\n    test('getImportSummary calculates correct statistics', () {\n      final originalList = [\n        const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),\n        const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),\n        const Spi(name: 'server3', ip: '192.168.1.300', port: 22, user: 'root'),\n        const Spi(name: 'duplicate', ip: '192.168.1.100', port: 22, user: 'root'), // Duplicate of server1\n      ];\n\n      final deduplicatedList = [\n        const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),\n        const Spi(name: 'server3', ip: '192.168.1.300', port: 22, user: 'root'),\n      ];\n\n      final summary = ServerDeduplication.getImportSummary(\n        originalList,\n        deduplicatedList,\n      );\n\n      expect(summary.total, 4);\n      expect(summary.duplicates, 2); // server1 and duplicate\n      expect(summary.toImport, 2);\n    });\n\n    test('getImportSummary handles case with no duplicates', () {\n      final originalList = [\n        const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),\n        const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),\n      ];\n\n      final summary = ServerDeduplication.getImportSummary(\n        originalList,\n        originalList,\n      );\n\n      expect(summary.total, 2);\n      expect(summary.duplicates, 0);\n      expect(summary.toImport, 2);\n    });\n\n    test('getImportSummary handles case with all duplicates', () {\n      final originalList = [\n        const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),\n        const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),\n      ];\n\n      final summary = ServerDeduplication.getImportSummary(\n        originalList,\n        [],\n      );\n\n      expect(summary.total, 2);\n      expect(summary.duplicates, 2);\n      expect(summary.toImport, 0);\n    });\n\n    test('getImportSummary handles empty lists', () {\n      final summary = ServerDeduplication.getImportSummary([], []);\n\n      expect(summary.total, 0);\n      expect(summary.duplicates, 0);\n      expect(summary.toImport, 0);\n    });\n\n    test('complete deduplication workflow', () {\n      // Simulate a complete import workflow\n      importedServers = [\n        const Spi(\n          name: 'production-web', // Name conflicts with existing\n          ip: '192.168.1.400', // Different IP, so not a duplicate\n          port: 22,\n          user: 'root',\n        ),\n        const Spi(\n          name: 'new-staging',\n          ip: '192.168.1.100', // Same as existing1, should be removed\n          port: 22,\n          user: 'root',\n        ),\n        const Spi(\n          name: 'unique-server',\n          ip: '192.168.1.500', // Unique server\n          port: 22,\n          user: 'admin',\n        ),\n      ];\n\n      // Step 1: Remove duplicates\n      final deduplicated = _mockDeduplicateServers(importedServers, existingServers);\n\n      expect(deduplicated, hasLength(2)); // new-staging should be removed\n\n      // Step 2: Resolve name conflicts\n      final resolved = _mockResolveNameConflicts(\n        deduplicated,\n        existingServers.map((s) => s.name).toList(),\n      );\n\n      expect(resolved, hasLength(2));\n      expect(resolved.any((s) => s.name == 'production-web (2)'), isTrue);\n      expect(resolved.any((s) => s.name == 'unique-server'), isTrue);\n\n      // Step 3: Get summary\n      final summary = ServerDeduplication.getImportSummary(\n        importedServers,\n        resolved,\n      );\n\n      expect(summary.total, 3);\n      expect(summary.duplicates, 1);\n      expect(summary.toImport, 2);\n    });\n\n    test('deduplication key generation is consistent', () {\n      const server1 = Spi(\n        name: 'test1',\n        ip: '192.168.1.100',\n        port: 22,\n        user: 'root',\n      );\n\n      const server2 = Spi(\n        name: 'test2', // Different name\n        ip: '192.168.1.100', // Same IP\n        port: 22, // Same port\n        user: 'root', // Same user\n      );\n\n      final servers = [server1, server2];\n      final deduplicated = _mockDeduplicateServers(servers, []);\n\n      // First server should be kept, second should be removed since it has same ip:port@user\n      expect(deduplicated, hasLength(1));\n      expect(deduplicated.first.name, 'test1');\n    });\n\n    test('ImportSummary properties work correctly', () {\n      final summary = ServerDeduplication.getImportSummary(\n        [\n          const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),\n          const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),\n        ],\n        [\n          const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),\n        ],\n      );\n\n      expect(summary.total, 2);\n      expect(summary.duplicates, 1);\n      expect(summary.toImport, 1);\n      expect(summary.hasDuplicates, isTrue);\n      expect(summary.hasItemsToImport, isTrue);\n    });\n\n    test('ImportSummary with no duplicates or imports', () {\n      final summary = ServerDeduplication.getImportSummary([], []);\n\n      expect(summary.total, 0);\n      expect(summary.duplicates, 0);\n      expect(summary.toImport, 0);\n      expect(summary.hasDuplicates, isFalse);\n      expect(summary.hasItemsToImport, isFalse);\n    });\n  });\n}"
  },
  {
    "path": "test/server_edit_logic_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/server_private_info.dart';\n\nvoid main() {\n  group('Server Edit Page Logic Tests', () {\n    test('SSH import should only be available on desktop platforms', () {\n      final desktopPlatforms = ['linux', 'macos', 'windows'];\n      final mobilePlatforms = ['android', 'ios', 'fuchsia'];\n      \n      for (final platform in desktopPlatforms) {\n        final isDesktop = desktopPlatforms.contains(platform);\n        expect(isDesktop, isTrue, reason: '$platform should support SSH import');\n      }\n      \n      for (final platform in mobilePlatforms) {\n        final isDesktop = desktopPlatforms.contains(platform);\n        expect(isDesktop, isFalse, reason: '$platform should not support SSH import');\n      }\n    });\n\n    test('permission prompt conditions are correct', () {\n      // Test the conditions for showing permission prompt\n      \n      // Should prompt when: firstTimeReadSSHCfg=true, sshConfigExists=true, isNewServer=true\n      bool shouldPrompt(bool firstTime, bool configExists, bool isNew) {\n        return firstTime && configExists && isNew;\n      }\n      \n      expect(shouldPrompt(true, true, true), isTrue);     // All conditions met\n      expect(shouldPrompt(false, true, true), isFalse);  // Setting disabled\n      expect(shouldPrompt(true, false, true), isFalse);  // No config file\n      expect(shouldPrompt(true, true, false), isFalse);  // Editing existing server\n      expect(shouldPrompt(false, false, false), isFalse); // No conditions met\n    });\n\n    test('server validation logic works correctly', () {\n      // Test server validation without actual form widgets\n      \n      // Valid server\n      const validServer = Spi(\n        name: 'test-server',\n        ip: '192.168.1.100',\n        port: 22,\n        user: 'root',\n      );\n      \n      expect(validServer.name.isNotEmpty, isTrue);\n      expect(validServer.ip.isNotEmpty, isTrue);\n      expect(validServer.port > 0 && validServer.port <= 65535, isTrue);\n      expect(validServer.user.isNotEmpty, isTrue);\n      \n      // Invalid cases\n      expect(''.isNotEmpty, isFalse); // Empty name\n      expect(0 > 0, isFalse); // Invalid port\n      expect(65536 <= 65535, isFalse); // Port too high\n    });\n\n    test('server form data processing is correct', () {\n      // Test data processing logic\n      \n      final Map<String, dynamic> formData = {\n        'name': 'my-server',\n        'ip': '192.168.1.100',\n        'port': '2222',\n        'user': 'admin',\n      };\n      \n      // Process form data into server object\n      final server = Spi(\n        name: formData['name'] as String,\n        ip: formData['ip'] as String,\n        port: int.parse(formData['port'] as String),\n        user: formData['user'] as String,\n      );\n      \n      expect(server.name, 'my-server');\n      expect(server.ip, '192.168.1.100');\n      expect(server.port, 2222);\n      expect(server.user, 'admin');\n    });\n\n    test('SSH key handling is correct', () {\n      // Test SSH key field handling\n      \n      const serverWithKey = Spi(\n        name: 'key-server',\n        ip: '192.168.1.100',\n        port: 22,\n        user: 'root',\n        keyId: '~/.ssh/id_rsa',\n      );\n      \n      expect(serverWithKey.keyId, '~/.ssh/id_rsa');\n      expect(serverWithKey.keyId?.isNotEmpty, isTrue);\n      \n      const serverWithoutKey = Spi(\n        name: 'pwd-server',\n        ip: '192.168.1.100',\n        port: 22,\n        user: 'root',\n        pwd: 'password123',\n      );\n      \n      expect(serverWithoutKey.keyId, isNull);\n      expect(serverWithoutKey.pwd, 'password123');\n    });\n\n    test('server editing vs creation logic', () {\n      // Test logic for distinguishing between editing and creating servers\n      \n      const existingServer = Spi(\n        name: 'existing',\n        ip: '192.168.1.100',\n        port: 22,\n        user: 'root',\n        id: 'server123',\n      );\n      \n      // Existing server has non-empty ID\n      final isEditing = existingServer.id.isNotEmpty;\n      final isCreating = !isEditing;\n      \n      expect(isEditing, isTrue);\n      expect(isCreating, isFalse);\n      \n      const newServer = Spi(\n        name: 'new-server',\n        ip: '192.168.1.100',\n        port: 22,\n        user: 'root',\n        id: '',\n      );\n      \n      final isCreatingNew = newServer.id.isEmpty;\n      final isEditingExisting = !isCreatingNew;\n      \n      expect(isCreatingNew, isTrue);\n      expect(isEditingExisting, isFalse);\n    });\n\n    test('form field population from imported server', () {\n      // Test that imported server data correctly populates form fields\n      \n      const importedServer = Spi(\n        name: 'imported-prod-web',\n        ip: '10.0.1.100',\n        port: 2222,\n        user: 'deploy',\n        keyId: '~/.ssh/production.pem',\n      );\n      \n      // Simulate form field population\n      final formFields = {\n        'name': importedServer.name,\n        'ip': importedServer.ip,\n        'port': importedServer.port.toString(),\n        'user': importedServer.user,\n        'keyId': importedServer.keyId,\n      };\n      \n      expect(formFields['name'], 'imported-prod-web');\n      expect(formFields['ip'], '10.0.1.100');\n      expect(formFields['port'], '2222');\n      expect(formFields['user'], 'deploy');\n      expect(formFields['keyId'], '~/.ssh/production.pem');\n    });\n\n    test('import summary display logic', () {\n      // Test import summary formatting\n      \n      const totalFound = 5;\n      const duplicatesRemoved = 2;\n      const serversToImport = 3;\n      \n      final summary = {\n        'total': totalFound,\n        'duplicates': duplicatesRemoved,\n        'toImport': serversToImport,\n      };\n      \n      expect(summary['total'], 5);\n      expect(summary['duplicates'], 2);\n      expect(summary['toImport'], 3);\n      \n      // Summary validation\n      expect(summary['duplicates']! + summary['toImport']!, summary['total']);\n      \n      // Format summary message (simplified)\n      final message = 'Found ${summary['total']} servers, '\n                     '${summary['duplicates']} duplicates removed, '\n                     '${summary['toImport']} will be imported.';\n      \n      expect(message, 'Found 5 servers, 2 duplicates removed, 3 will be imported.');\n    });\n\n    test('error handling logic', () {\n      // Test error handling scenarios\n      \n      final errors = <String>[];\n      \n      // Validation errors\n      void validateServer(Spi server) {\n        if (server.name.isEmpty) {\n          errors.add('Server name is required');\n        }\n        if (server.ip.isEmpty) {\n          errors.add('Server IP is required');\n        }\n        if (server.port <= 0 || server.port > 65535) {\n          errors.add('Port must be between 1 and 65535');\n        }\n        if (server.user.isEmpty) {\n          errors.add('Username is required');\n        }\n      }\n      \n      // Test with invalid server\n      const invalidServer = Spi(\n        name: '',\n        ip: '',\n        port: 0,\n        user: '',\n      );\n      \n      validateServer(invalidServer);\n      \n      expect(errors.length, 4);\n      expect(errors.contains('Server name is required'), isTrue);\n      expect(errors.contains('Server IP is required'), isTrue);\n      expect(errors.contains('Port must be between 1 and 65535'), isTrue);\n      expect(errors.contains('Username is required'), isTrue);\n      \n      // Test with valid server\n      errors.clear();\n      const validServer = Spi(\n        name: 'valid',\n        ip: '192.168.1.1',\n        port: 22,\n        user: 'root',\n      );\n      \n      validateServer(validServer);\n      expect(errors.isEmpty, isTrue);\n    });\n\n    test('name conflict resolution logic', () {\n      // Test name conflict resolution during import\n      \n      final existingNames = ['server1', 'server2', 'server3'];\n      \n      String resolveNameConflict(String proposedName, List<String> existing) {\n        if (!existing.contains(proposedName)) {\n          return proposedName;\n        }\n        \n        int suffix = 2;\n        String newName;\n        do {\n          newName = '$proposedName ($suffix)';\n          suffix++;\n        } while (existing.contains(newName));\n        \n        return newName;\n      }\n      \n      // Test with no conflict\n      expect(resolveNameConflict('unique-name', existingNames), 'unique-name');\n      \n      // Test with conflict\n      expect(resolveNameConflict('server1', existingNames), 'server1 (2)');\n      \n      // Test with multiple conflicts\n      final extendedNames = [...existingNames, 'server1 (2)'];\n      expect(resolveNameConflict('server1', extendedNames), 'server1 (3)');\n    });\n\n    test('SSH config import button visibility logic', () {\n      // Test when SSH import button should be visible\n      \n      bool shouldShowSSHImport({\n        required bool isDesktop,\n        required bool firstTimeReadSSHCfg,\n        required bool isNewServer,\n      }) {\n        return isDesktop && (firstTimeReadSSHCfg || !isNewServer);\n      }\n      \n      // Desktop, first time, new server - should show\n      expect(shouldShowSSHImport(\n        isDesktop: true,\n        firstTimeReadSSHCfg: true,\n        isNewServer: true,\n      ), isTrue);\n      \n      // Desktop, not first time, new server - should not show auto import but manual import available\n      expect(shouldShowSSHImport(\n        isDesktop: true,\n        firstTimeReadSSHCfg: false,\n        isNewServer: true,\n      ), isFalse);\n      \n      // Desktop, editing existing server - should show manual import\n      expect(shouldShowSSHImport(\n        isDesktop: true,\n        firstTimeReadSSHCfg: false,\n        isNewServer: false,\n      ), isTrue);\n      \n      // Mobile - should never show\n      expect(shouldShowSSHImport(\n        isDesktop: false,\n        firstTimeReadSSHCfg: true,\n        isNewServer: true,\n      ), isFalse);\n    });\n  });\n}"
  },
  {
    "path": "test/ssh_config_test.dart",
    "content": "import 'dart:io';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/core/utils/ssh_config.dart';\n\nvoid main() {\n  group('SSHConfig Tests', () {\n    late Directory tempDir;\n    late File configFile;\n\n    setUp(() async {\n      // Create temporary directory for test SSH config files\n      tempDir = await Directory.systemTemp.createTemp('ssh_config_test');\n      configFile = File('${tempDir.path}/config');\n    });\n\n    tearDown(() async {\n      // Clean up temporary files\n      if (tempDir.existsSync()) {\n        await tempDir.delete(recursive: true);\n      }\n    });\n\n    test('configExists returns false for non-existent file', () async {\n      final (_, exists) = SSHConfig.configExists('/non/existent/path');\n      expect(exists, false);\n    });\n\n    test('configExists returns true for existing file', () async {\n      await configFile.writeAsString('Host example\\n  HostName example.com\\n');\n      final (_, exists) = SSHConfig.configExists(configFile.path);\n      expect(exists, true);\n    });\n\n    test('parseConfig handles empty file', () async {\n      await configFile.writeAsString('');\n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, isEmpty);\n    });\n\n    test('parseConfig handles file with only comments', () async {\n      await configFile.writeAsString('''\n# This is a comment\n# Another comment\n''');\n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, isEmpty);\n    });\n\n    test('parseConfig parses single host correctly', () async {\n      await configFile.writeAsString('''\nHost myserver\n  HostName 192.168.1.100\n  User admin\n  Port 2222\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(1));\n      \n      final server = servers.first;\n      expect(server.name, 'myserver');\n      expect(server.ip, '192.168.1.100');\n      expect(server.user, 'admin');\n      expect(server.port, 2222);\n    });\n\n    test('parseConfig handles missing HostName', () async {\n      await configFile.writeAsString('''\nHost myserver\n  User admin\n  Port 2222\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, isEmpty); // Should skip hosts without HostName\n    });\n\n    test('parseConfig uses defaults for missing optional fields', () async {\n      await configFile.writeAsString('''\nHost simple\n  HostName example.com\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(1));\n      \n      final server = servers.first;\n      expect(server.name, 'simple');\n      expect(server.ip, 'example.com');\n      expect(server.user, 'root'); // default user\n      expect(server.port, 22);     // default port\n    });\n\n    test('parseConfig handles multiple hosts', () async {\n      await configFile.writeAsString('''\nHost server1\n  HostName 192.168.1.100\n  User alice\n  Port 22\n\nHost server2\n  HostName 192.168.1.200\n  User bob\n  Port 2222\n\nHost server3\n  HostName example.com\n  User charlie\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(3));\n      \n      expect(servers[0].name, 'server1');\n      expect(servers[0].ip, '192.168.1.100');\n      expect(servers[0].user, 'alice');\n      expect(servers[0].port, 22);\n      \n      expect(servers[1].name, 'server2');\n      expect(servers[1].ip, '192.168.1.200');\n      expect(servers[1].user, 'bob');\n      expect(servers[1].port, 2222);\n      \n      expect(servers[2].name, 'server3');\n      expect(servers[2].ip, 'example.com');\n      expect(servers[2].user, 'charlie');\n      expect(servers[2].port, 22);\n    });\n\n    test('parseConfig handles case insensitive keywords', () async {\n      await configFile.writeAsString('''\nhost myserver\n  hostname 192.168.1.100\n  user admin\n  port 2222\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(1));\n      \n      final server = servers.first;\n      expect(server.name, 'myserver');\n      expect(server.ip, '192.168.1.100');\n      expect(server.user, 'admin');\n      expect(server.port, 2222);\n    });\n\n    test('parseConfig handles comments and empty lines', () async {\n      await configFile.writeAsString('''\n# Global settings\nHost *\n  ServerAliveInterval 60\n\n# My development server\nHost devserver\n  HostName 192.168.1.50\n  User developer  # development user\n  Port 22\n\n# Empty line below\n\nHost prodserver\n  HostName 10.0.0.100\n  User production\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(2));\n      \n      expect(servers[0].name, 'devserver');\n      expect(servers[0].ip, '192.168.1.50');\n      expect(servers[0].user, 'developer');\n      \n      expect(servers[1].name, 'prodserver');\n      expect(servers[1].ip, '10.0.0.100');\n      expect(servers[1].user, 'production');\n    });\n\n    test('parseConfig handles wildcard hosts', () async {\n      await configFile.writeAsString('''\nHost *\n  User defaultuser\n  Port 2222\n\nHost myserver\n  HostName 192.168.1.100\n  User admin\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(1)); // Only named hosts, not wildcards\n      \n      final server = servers.first;\n      expect(server.name, 'myserver');\n      expect(server.ip, '192.168.1.100');\n      expect(server.user, 'admin');\n      expect(server.port, 22); // Uses default, not wildcard setting\n    });\n\n    test('parseConfig handles IdentityFile', () async {\n      await configFile.writeAsString('''\nHost keyserver\n  HostName 192.168.1.100\n  User admin\n  IdentityFile ~/.ssh/special_key\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(1));\n      \n      final server = servers.first;\n      expect(server.keyId, '~/.ssh/special_key');\n    });\n\n    test('parseConfig handles quoted values', () async {\n      await configFile.writeAsString('''\nHost \"server with spaces\"\n  HostName \"192.168.1.100\"\n  User \"admin user\"\n  IdentityFile \"~/.ssh/key with spaces\"\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(1));\n      \n      final server = servers.first;\n      expect(server.name, 'server with spaces');\n      expect(server.ip, '192.168.1.100');\n      expect(server.user, 'admin user');\n      expect(server.keyId, '~/.ssh/key with spaces');\n    });\n\n    test('parseConfig handles invalid port values', () async {\n      await configFile.writeAsString('''\nHost badport\n  HostName 192.168.1.100\n  Port notanumber\n\nHost goodserver\n  HostName 192.168.1.200\n  Port 2222\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(2));\n      \n      // First server should use default port due to invalid port\n      expect(servers[0].name, 'badport');\n      expect(servers[0].port, 22); // default port\n      \n      // Second server should use specified port\n      expect(servers[1].name, 'goodserver');\n      expect(servers[1].port, 2222);\n    });\n\n    test('parseConfig skips hosts with multiple host patterns', () async {\n      await configFile.writeAsString('''\nHost server1 server2\n  HostName 192.168.1.100\n\nHost singleserver\n  HostName 192.168.1.200\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(1)); // Only single host patterns\n      \n      expect(servers[0].name, 'singleserver');\n    });\n\n    test('parseConfig handles ProxyJump (ignored)', () async {\n      await configFile.writeAsString('''\nHost jumpserver\n  HostName 192.168.1.100\n  User admin\n  ProxyJump bastion.example.com\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(1));\n      \n      final server = servers.first;\n      expect(server.name, 'jumpserver');\n      expect(server.ip, '192.168.1.100');\n      expect(server.user, 'admin');\n      // ProxyJump is ignored in current implementation\n    });\n\n    test('parseConfig returns empty list for non-existent file', () async {\n      final servers = await SSHConfig.parseConfig('/non/existent/path');\n      expect(servers, isEmpty);\n    });\n\n    test('parseConfig handles real-world SSH config example', () async {\n      await configFile.writeAsString('''\n# Default settings for all hosts\nHost *\n    ServerAliveInterval 60\n    ServerAliveCountMax 3\n    TCPKeepAlive yes\n\n# Production servers\nHost prod-web-01\n    HostName 10.0.1.100\n    User deploy\n    Port 22\n    IdentityFile ~/.ssh/production.pem\n\nHost prod-db-01\n    HostName 10.0.1.200\n    User ubuntu\n    Port 2222\n    IdentityFile ~/.ssh/production.pem\n\n# Development environment\nHost dev\n    HostName dev.example.com\n    User developer\n    Port 22\n\n# Jump host configuration\nHost bastion\n    HostName bastion.example.com\n    User ec2-user\n    IdentityFile ~/.ssh/bastion.pem\n\nHost internal-server\n    HostName 172.16.0.50\n    User admin\n    ProxyJump bastion\n''');\n      \n      final servers = await SSHConfig.parseConfig(configFile.path);\n      expect(servers, hasLength(5));\n      \n      // Check specific servers\n      final prodWeb = servers.firstWhere((s) => s.name == 'prod-web-01');\n      expect(prodWeb.ip, '10.0.1.100');\n      expect(prodWeb.user, 'deploy');\n      expect(prodWeb.port, 22);\n      expect(prodWeb.keyId, '~/.ssh/production.pem');\n      \n      final prodDb = servers.firstWhere((s) => s.name == 'prod-db-01');\n      expect(prodDb.ip, '10.0.1.200');\n      expect(prodDb.user, 'ubuntu');\n      expect(prodDb.port, 2222);\n      \n      final dev = servers.firstWhere((s) => s.name == 'dev');\n      expect(dev.ip, 'dev.example.com');\n      expect(dev.user, 'developer');\n      expect(dev.port, 22);\n      expect(dev.keyId, isNull);\n    });\n  });\n}"
  },
  {
    "path": "test/system_dist_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/server/dist.dart';\nimport 'package:server_box/data/model/server/system.dart';\n\nvoid main() {\n  group('SystemType Tests', () {\n    test('SystemType values and properties', () {\n      expect(SystemType.linux.value, equals('__linux'));\n      expect(SystemType.bsd.value, equals('__bsd'));\n      expect(SystemType.windows.value, equals('__windows'));\n    });\n\n    test('SystemType.parse detects Windows', () {\n      const windowsOutput = 'System info __windows version 10';\n      final result = SystemType.parse(windowsOutput);\n      expect(result, equals(SystemType.windows));\n    });\n\n    test('SystemType.parse detects BSD', () {\n      const bsdOutput = 'FreeBSD 13.0-RELEASE __bsd';\n      final result = SystemType.parse(bsdOutput);\n      expect(result, equals(SystemType.bsd));\n    });\n\n    test('SystemType.parse detects Linux', () {\n      const linuxOutput = 'Linux 5.4.0 __linux';\n      final result = SystemType.parse(linuxOutput);\n      expect(result, equals(SystemType.linux));\n    });\n\n    test('SystemType.parse defaults to Linux for empty input', () {\n      const emptyOutput = '';\n      final result = SystemType.parse(emptyOutput);\n      expect(result, equals(SystemType.linux));\n    });\n\n    test('SystemType.parse defaults to Linux for unknown output', () {\n      const unknownOutput = 'Unknown system output';\n      final result = SystemType.parse(unknownOutput);\n      expect(result, equals(SystemType.linux));\n    });\n\n    test('SystemType.parse handles long strings', () {\n      final longOutput = 'a' * 200;\n      final result = SystemType.parse(longOutput);\n      expect(result, equals(SystemType.linux));\n    });\n  });\n\n  group('Dist Tests', () {\n    test('Dist enum contains all expected values', () {\n      expect(Dist.values, contains(Dist.debian));\n      expect(Dist.values, contains(Dist.ubuntu));\n      expect(Dist.values, contains(Dist.centos));\n      expect(Dist.values, contains(Dist.fedora));\n      expect(Dist.values, contains(Dist.opensuse));\n      expect(Dist.values, contains(Dist.kali));\n      expect(Dist.values, contains(Dist.wrt));\n      expect(Dist.values, contains(Dist.armbian));\n      expect(Dist.values, contains(Dist.arch));\n      expect(Dist.values, contains(Dist.alpine));\n      expect(Dist.values, contains(Dist.rocky));\n      expect(Dist.values, contains(Dist.deepin));\n      expect(Dist.values, contains(Dist.coreelec));\n    });\n\n    test('String.dist extension detects Debian', () {\n      const input = 'debian 10.0';\n      final result = input.dist;\n      expect(result, equals(Dist.debian));\n    });\n\n    test('String.dist extension detects Ubuntu', () {\n      const input = 'ubuntu 20.04';\n      final result = input.dist;\n      expect(result, equals(Dist.ubuntu));\n    });\n\n    test('String.dist extension detects CentOS', () {\n      const input = 'centos 8';\n      final result = input.dist;\n      expect(result, equals(Dist.centos));\n    });\n\n    test('String.dist extension detects Fedora', () {\n      const input = 'fedora 34';\n      final result = input.dist;\n      expect(result, equals(Dist.fedora));\n    });\n\n    test('String.dist extension detects OpenSUSE', () {\n      const input = 'opensuse leap 15.3';\n      final result = input.dist;\n      expect(result, equals(Dist.opensuse));\n    });\n\n    test('String.dist extension detects Kali', () {\n      const input = 'kali linux 2021.2';\n      final result = input.dist;\n      expect(result, equals(Dist.kali));\n    });\n\n    test('String.dist extension detects WRT special case', () {\n      const input = 'istoreos 21.02';\n      final result = input.dist;\n      expect(result, equals(Dist.wrt));\n    });\n\n    test('String.dist extension detects Armbian', () {\n      const input = 'armbian 21.08';\n      final result = input.dist;\n      expect(result, equals(Dist.armbian));\n    });\n\n    test('String.dist extension detects Arch', () {\n      const input = 'arch linux';\n      final result = input.dist;\n      expect(result, equals(Dist.arch));\n    });\n\n    test('String.dist extension detects Alpine', () {\n      const input = 'alpine linux 3.14';\n      final result = input.dist;\n      expect(result, equals(Dist.alpine));\n    });\n\n    test('String.dist extension detects Rocky', () {\n      const input = 'rocky linux 8.4';\n      final result = input.dist;\n      expect(result, equals(Dist.rocky));\n    });\n\n    test('String.dist extension detects Deepin', () {\n      const input = 'deepin 20';\n      final result = input.dist;\n      expect(result, equals(Dist.deepin));\n    });\n\n    test('String.dist extension detects CoreELEC', () {\n      const input = 'coreelec 19.0';\n      final result = input.dist;\n      expect(result, equals(Dist.coreelec));\n    });\n\n    test('String.dist extension is case insensitive', () {\n      const input = 'UBUNTU 20.04';\n      final result = input.dist;\n      expect(result, equals(Dist.ubuntu));\n    });\n\n    test('String.dist extension returns null for unknown distribution', () {\n      const input = 'unknown distribution';\n      final result = input.dist;\n      expect(result, isNull);\n    });\n\n    test('String.dist extension handles empty string', () {\n      const input = '';\n      final result = input.dist;\n      expect(result, isNull);\n    });\n\n    test('String.dist extension handles substring matches', () {\n      const input = 'xubuntu 20.04'; // Contains 'ubuntu'\n      final result = input.dist;\n      expect(result, equals(Dist.ubuntu));\n    });\n\n    test('String.dist extension prioritizes exact matches over substring', () {\n      const input = 'kali linux'; // Contains 'linux' but 'kali' is more specific\n      final result = input.dist;\n      expect(result, equals(Dist.kali));\n    });\n\n    test('Special WRT cases are handled correctly', () {\n      expect('istoreos'.dist, equals(Dist.wrt));\n      expect('iStoreOS'.dist, equals(Dist.wrt));\n      expect('ISTOREOS'.dist, equals(Dist.wrt));\n    });\n\n    test('Distribution detection order follows enum order', () {\n      // This test ensures that the first match in the enum is returned\n      const input = 'ubuntu'; // This could match both 'ubuntu' and 'kali' (contains 'u')\n      final result = input.dist;\n      expect(result, equals(Dist.ubuntu)); // ubuntu comes before kali in enum\n    });\n  });\n}"
  },
  {
    "path": "test/uptime_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Linux uptime parsing tests', () {\n    test('should parse uptime with days and hours:minutes', () {\n      const raw = '19:39:15 up 61 days, 18:16,  1 user,  load average: 0.00, 0.00, 0.00';\n      final result = _testParseUpTime(raw);\n      expect(result, '61 days, 18:16');\n    });\n\n    test('should parse uptime with single day and hours:minutes', () {\n      const raw = '19:39:15 up 1 day, 2:34,  1 user,  load average: 0.00, 0.00, 0.00';\n      final result = _testParseUpTime(raw);\n      expect(result, '1 day, 2:34');\n    });\n\n    test('should parse uptime with only hours:minutes', () {\n      const raw = '19:39:15 up 2:34,  1 user,  load average: 0.00, 0.00, 0.00';\n      final result = _testParseUpTime(raw);\n      expect(result, '2:34');\n    });\n\n    test('should parse uptime with only minutes', () {\n      const raw = '19:39:15 up 34 min,  1 user,  load average: 0.00, 0.00, 0.00';\n      final result = _testParseUpTime(raw);\n      expect(result, '34 min');\n    });\n\n    test('should parse uptime with days only (no time part)', () {\n      const raw = '19:39:15 up 5 days,  1 user,  load average: 0.00, 0.00, 0.00';\n      final result = _testParseUpTime(raw);\n      expect(result, '5 days');\n    });\n\n    test('should return null for invalid format', () {\n      const raw = 'invalid uptime format';\n      final result = _testParseUpTime(raw);\n      expect(result, null);\n    });\n\n    test('should handle edge case with empty string', () {\n      const raw = '';\n      final result = _testParseUpTime(raw);\n      expect(result, null);\n    });\n  });\n}\n\n// Helper function to test the private _parseUpTime function\nString? _testParseUpTime(String raw) {\n  final splitedUp = raw.split('up ');\n  if (splitedUp.length == 2) {\n    final uptimePart = splitedUp[1];\n    final splitedComma = uptimePart.split(', ');\n    \n    if (splitedComma.isEmpty) return null;\n    \n    // Handle different uptime formats\n    final firstPart = splitedComma[0].trim();\n    \n    // Case 1: \"61 days\" or \"1 day\" - need to get the time part from next segment\n    if (firstPart.contains('day')) {\n      if (splitedComma.length >= 2) {\n        final timePart = splitedComma[1].trim();\n        // Check if it's in HH:MM format\n        if (timePart.contains(':') && !timePart.contains('user') && !timePart.contains('load')) {\n          return '$firstPart, $timePart';\n        }\n      }\n      return firstPart;\n    }\n    \n    // Case 2: \"2:34\" (hours:minutes) - already in good format\n    if (firstPart.contains(':') && !firstPart.contains('user') && !firstPart.contains('load')) {\n      return firstPart;\n    }\n    \n    // Case 3: \"34 min\" - already in good format  \n    if (firstPart.contains('min')) {\n      return firstPart;\n    }\n    \n    // Fallback: return first part\n    return firstPart;\n  }\n  return null;\n}"
  },
  {
    "path": "test/windows_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:server_box/data/model/app/scripts/cmd_types.dart';\nimport 'package:server_box/data/model/app/scripts/script_builders.dart';\nimport 'package:server_box/data/model/app/scripts/shell_func.dart';\nimport 'package:server_box/data/model/server/server_status_update_req.dart';\nimport 'package:server_box/data/model/server/system.dart';\nimport 'package:server_box/data/res/status.dart';\n\nvoid main() {\n  group('Windows System Tests', () {\n    test('should verify Windows segments length matches command types', () {\n      expect(WindowsStatusCmdType.values.length, isPositive);\n    });\n\n    test('should generate Windows PowerShell script correctly', () {\n      final builder = ScriptBuilderFactory.getBuilder(true);\n      final script = builder.buildScript(null);\n      \n      expect(script, contains('PowerShell script for ServerBox'));\n      expect(script, contains('switch (\\$args[0])'));\n      expect(script, contains('-${ShellFunc.status.flag}'));\n    });\n\n    test('should handle Windows system parsing with real data', () async {\n      final serverStatus = InitStatus.status;\n\n      final req = ServerStatusUpdateReq(\n        system: SystemType.windows,\n        ss: serverStatus,\n        parsedOutput: {}, // Empty for legacy tests\n        customCmds: {},\n      );\n\n      final result = await getStatus(req);\n\n      // Basic validation that result is not null\n      expect(result, isNotNull);\n    });\n\n    test('should parse Windows CPU data correctly', () async {\n      final serverStatus = InitStatus.status;\n\n      final req = ServerStatusUpdateReq(\n        system: SystemType.windows,\n        ss: serverStatus,\n        parsedOutput: {}, // Empty for legacy tests\n        customCmds: {},\n      );\n\n      // Should not throw exceptions\n      expect(() async => await getStatus(req), returnsNormally);\n    });\n\n    test('should parse Windows memory data correctly', () async {\n      final serverStatus = InitStatus.status;\n\n      final req = ServerStatusUpdateReq(\n        system: SystemType.windows,\n        ss: serverStatus,\n        parsedOutput: {}, // Empty for legacy tests\n        customCmds: {},\n      );\n\n      // Should not throw exceptions  \n      expect(() async => await getStatus(req), returnsNormally);\n    });\n\n    test('should parse Windows disk data correctly', () async {\n      final serverStatus = InitStatus.status;\n\n      final req = ServerStatusUpdateReq(\n        system: SystemType.windows,\n        ss: serverStatus,\n        parsedOutput: {}, // Empty for legacy tests\n        customCmds: {},\n      );\n\n      // Should not throw exceptions\n      expect(() async => await getStatus(req), returnsNormally);\n    });\n\n    test('should parse Windows battery data correctly', () async {\n      final serverStatus = InitStatus.status;\n\n      final req = ServerStatusUpdateReq(\n        system: SystemType.windows,\n        ss: serverStatus,\n        parsedOutput: {}, // Empty for legacy tests\n        customCmds: {},\n      );\n\n      // Should not throw exceptions\n      expect(() async => await getStatus(req), returnsNormally);\n    });\n\n    test('should handle Windows uptime parsing correctly', () async {\n      final serverStatus = InitStatus.status;\n\n      final req = ServerStatusUpdateReq(\n        system: SystemType.windows,\n        ss: serverStatus,\n        parsedOutput: {}, // Empty for legacy tests\n        customCmds: {},\n      );\n\n      // Should not throw exceptions\n      expect(() async => await getStatus(req), returnsNormally);\n    });\n\n    test('should handle Windows uptime parsing with old format', () async {\n      final serverStatus = InitStatus.status;\n\n      final req = ServerStatusUpdateReq(\n        system: SystemType.windows,\n        ss: serverStatus,\n        parsedOutput: {}, // Empty for legacy tests  \n        customCmds: {},\n      );\n\n      // Should not throw exceptions\n      expect(() async => await getStatus(req), returnsNormally);\n    });\n\n    test('should handle Windows script path generation', () {\n      final scriptPath = ShellFunc.status.exec('test-server', systemType: SystemType.windows, customDir: null);\n      \n      expect(scriptPath, contains('powershell'));\n      expect(scriptPath, contains('-ExecutionPolicy Bypass'));\n      expect(scriptPath, contains('-${ShellFunc.status.flag}'));\n    });\n\n    test('should execute Windows commands correctly', () {\n      for (final func in ShellFunc.values) {\n        final command = func.exec('test-server', systemType: SystemType.windows, customDir: null);\n        expect(command, isNotEmpty);\n        expect(command, contains('powershell'));\n      }\n    });\n\n    test('should handle GPU detection on Windows', () async {\n      final serverStatus = InitStatus.status;\n\n      final req = ServerStatusUpdateReq(\n        system: SystemType.windows,\n        ss: serverStatus,\n        parsedOutput: {}, // Empty for legacy tests\n        customCmds: {},\n      );\n\n      // Should handle NVIDIA driver not found gracefully\n      expect(() async => await getStatus(req), returnsNormally);\n    });\n\n    test('should handle Windows error conditions gracefully', () async {\n      final serverStatus = InitStatus.status;\n\n      final req = ServerStatusUpdateReq(\n        system: SystemType.windows,\n        ss: serverStatus,\n        parsedOutput: {}, // Empty for legacy tests\n        customCmds: {},\n      );\n\n      // Should not throw exceptions even with error conditions\n      expect(() async => await getStatus(req), returnsNormally);\n    });\n\n    test('should handle Windows temperature error output gracefully', () async {\n      final serverStatus = InitStatus.status;\n\n      final req = ServerStatusUpdateReq(\n        system: SystemType.windows,\n        ss: serverStatus,\n        parsedOutput: {}, // Empty for legacy tests\n        customCmds: {},\n      );\n\n      // Should not throw exceptions even with error output in temperature values\n      expect(() async => await getStatus(req), returnsNormally);\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": "cmake_minimum_required(VERSION 3.14)\nproject(toolbox LANGUAGES CXX)\n\nset(BINARY_NAME \"ServerBox\")\n\ncmake_policy(SET CMP0063 NEW)\n\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Configure build options.\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\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.\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\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\n\n# Flutter library and tool build rules.\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build\nadd_subdirectory(\"runner\")\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": "cmake_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 <app_links/app_links_plugin_c_api.h>\n#include <dynamic_color/dynamic_color_plugin_c_api.h>\n#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>\n#include <local_auth_windows/local_auth_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 <window_manager/window_manager_plugin.h>\n\nvoid RegisterPlugins(flutter::PluginRegistry* registry) {\n  AppLinksPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"AppLinksPluginCApi\"));\n  DynamicColorPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"DynamicColorPluginCApi\"));\n  FlutterSecureStorageWindowsPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"FlutterSecureStorageWindowsPlugin\"));\n  LocalAuthPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"LocalAuthPlugin\"));\n  ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"ScreenRetrieverWindowsPluginCApi\"));\n  SharePlusWindowsPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"SharePlusWindowsPluginCApi\"));\n  UrlLauncherWindowsRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"UrlLauncherWindows\"));\n  WindowManagerPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"WindowManagerPlugin\"));\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  app_links\n  dynamic_color\n  flutter_secure_storage_windows\n  local_auth_windows\n  screen_retriever_windows\n  share_plus\n  url_launcher_windows\n  window_manager\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/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(runner LANGUAGES CXX)\n\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)\napply_standard_settings(${BINARY_NAME})\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\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\", \"tech.lolli\" \"\\0\"\n            VALUE \"FileDescription\", \"ServerBox\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"toolbox\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2022 tech.lolli. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"toolbox.exe\" \"\\0\"\n            VALUE \"ProductName\", \"toolbox\" \"\\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  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(400, 777);\n  if (!window.CreateAndShow(L\"ServerBox\", 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 -->\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  if (target_length == 0) {\n    return std::string();\n  }\n  std::string utf8_string;\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, utf8_string.data(),\n      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 <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\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    FreeLibrary(user32_module);\n  }\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 registar 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::CreateAndShow(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  return OnCreate();\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\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"
  },
  {
    "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 and shows a win32 window with |title| and position and size 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 to will treat the width height passed in to this function\n  // as logical pixels and scale to appropriate for the default monitor. Returns\n  // true if the window was created successfully.\n  bool CreateAndShow(const std::wstring& title,\n                     const Point& origin,\n                     const Size& size);\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  // responsponds 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  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"
  }
]